diff --git a/README.md b/README.md index b2d13f8..eb312f3 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Code is highly experimental and prone to change in the future. I will keep alter - [x] Kempston mouse. - [x] Kempston/Fuller/Cursor/Sinclair joysticks. - [x] RF/CRT experience (not physically accurate though). -- [x] TAP/TZX/PZX/CSW tapes. Z80/SNA snaps. ROM/IF2 roms. +- [x] TAP/TZX/PZX/CSW tapes. Z80/SNA snaps. ROM/IF2 roms. - [x] DSK/EDSK/TRD/SCL/FDI/MGT/IMG/HOBETA disks. - [x] SCR/PNG screenshots. - [x] ZIP/RAR/GZ archives. @@ -83,7 +83,7 @@ This software is released into the [public domain](https://unlicense.org/). Also - [SpecEmu](https://specemu.zxe.io/), my favourite ZX emulator on Windows. - [SpectrumComputing](https://spectrumcomputing.co.uk/), [WorldOfSpectrum](https://worldofspectrum.net/), [ZXArt](https://zxart.ee/), [Virtual TRDOS](https://vtrd.in/) and [ZXInfo](https://zxinfo.dk/) are the best online resources (imho). - [Crash](https://archive.org/details/crash-magazine), [YourSinclair](https://archive.org/details/your-sinclair-magazine), [SinclairUser](https://archive.org/details/sinclair-user-magazine) and [MicroHobby(ES)](https://archive.org/details/microhobby-magazine) are great old paper magazines. -- [ZXDB](https://github.com/zxdb/ZXDB), [game maps](https://maps.speccy.cz/), [game cheats](https://www.the-tipshop.co.uk/) and [game longplays](https://www.youtube.com/@rzxarchive). +- [ZXDB](https://github.com/zxdb/ZXDB), [game maps](https://maps.speccy.cz/), [game cheats](https://www.the-tipshop.co.uk/), [RZX](https://worldofspectrum.net/RZXformat.html)[replays](https://www.rzxarchive.co.uk/) and [game longplays](https://www.youtube.com/@rzxarchive). - [Daily ZX videos](https://www.youtube.com/results?search_query=zx+spectrum&sp=CAI%253D), on YouTube. [![](https://github.com/r-lyeh/Spectral/actions/workflows/build.yml/badge.svg)](https://github.com/r-lyeh/Spectral/actions/workflows/build.yml) diff --git a/src/3rd_deflate.h b/src/3rd_deflate.h index 71333d3..399f192 100644 --- a/src/3rd_deflate.h +++ b/src/3rd_deflate.h @@ -63,6 +63,7 @@ typedef uint32_t mz_uint; #define MZ_MAX(a,b) (((a)>(b))?(a):(b)) #define MZ_MIN(a,b) (((a)<(b))?(a):(b)) #define MZ_CLEAR_OBJ(obj) memset(&(obj), 0, sizeof(obj)) +#define MZ_CLEAR_ARR(obj) memset((obj), 0, sizeof(obj)) #if MINIZ_USE_UNALIGNED_LOADS_AND_STORES && MINIZ_LITTLE_ENDIAN #define MZ_READ_LE16(p) *((const uint16_t *)(p)) @@ -556,6 +557,9 @@ typedef enum TDEFL_FINISH = 4 } tdefl_flush; +/* Output stream interface. The compressor uses this interface to write compressed data. It'll typically be called TDEFL_OUT_BUF_SIZE at a time. */ +typedef mz_bool (*tdefl_put_buf_func_ptr)(const void *pBuf, int len, void *pUser); + // tdefl's compression state structure. typedef struct { @@ -581,6 +585,10 @@ typedef struct uint16_t m_next[TDEFL_LZ_DICT_SIZE]; uint16_t m_hash[TDEFL_LZ_HASH_SIZE]; uint8_t m_output_buf[TDEFL_OUT_BUF_SIZE]; + + tdefl_put_buf_func_ptr m_pPut_buf_func; + void *m_pPut_buf_user; + } tdefl_compressor; // ------------------- zlib-style API's @@ -1539,24 +1547,44 @@ tdefl_status tdefl_compress_buffer(tdefl_compressor *d, const void *pIn_buf, siz return tdefl_compress(d, pIn_buf, &in_buf_size, NULL, NULL, flush); } -tdefl_status tdefl_init(tdefl_compressor *d, void *out, size_t outlen, int flags) +tdefl_status tdefl_init(tdefl_compressor *d, tdefl_put_buf_func_ptr pPut_buf_func, void *pPut_buf_user, int flags) { -#if 0 - d->m_putbuf_callback = putbuf_callback; d->m_pPut_buf_user = pPut_buf_user; - d->m_flags = (mz_uint)(flags); d->m_max_probes[0] = 1 + ((flags & 0xFFF) + 2) / 3; d->m_greedy_parsing = (flags & TDEFL_GREEDY_PARSING_FLAG) != 0; + d->m_pPut_buf_func = pPut_buf_func; + d->m_pPut_buf_user = pPut_buf_user; + d->m_flags = (mz_uint)(flags); + d->m_max_probes[0] = 1 + ((flags & 0xFFF) + 2) / 3; + d->m_greedy_parsing = (flags & TDEFL_GREEDY_PARSING_FLAG) != 0; d->m_max_probes[1] = 1 + (((flags & 0xFFF) >> 2) + 2) / 3; - if (!(flags & TDEFL_NONDETERMINISTIC_PARSING_FLAG)) MZ_CLEAR_OBJ(d->m_hash); + if (!(flags & TDEFL_NONDETERMINISTIC_PARSING_FLAG)) + MZ_CLEAR_ARR(d->m_hash); d->m_lookahead_pos = d->m_lookahead_size = d->m_dict_size = d->m_total_lz_bytes = d->m_lz_code_buf_dict_pos = d->m_bits_in = 0; d->m_output_flush_ofs = d->m_output_flush_remaining = d->m_finished = d->m_block_index = d->m_bit_buffer = d->m_wants_to_finish = 0; - d->m_pLZ_code_buf = d->m_lz_code_buf + 1; d->m_pLZ_flags = d->m_lz_code_buf; d->m_num_flags_left = 8; - d->m_pOutput_buf = d->m_output_buf; d->m_pOutput_buf_end = d->m_output_buf; d->m_prev_return_status = TDEFL_STATUS_OKAY; - d->m_saved_match_dist = d->m_saved_match_len = d->m_saved_lit = 0; d->m_adler32 = 1; - d->m_pIn_buf = NULL; d->m_pOut_buf = NULL; - d->m_pIn_buf_size = NULL; d->m_pOut_buf_size = NULL; - d->m_flush = TDEFL_NO_FLUSH; d->m_pSrc = NULL; d->m_src_buf_left = 0; d->m_out_buf_ofs = 0; + d->m_pLZ_code_buf = d->m_lz_code_buf + 1; + d->m_pLZ_flags = d->m_lz_code_buf; + *d->m_pLZ_flags = 0; + d->m_num_flags_left = 8; + d->m_pOutput_buf = d->m_output_buf; + d->m_pOutput_buf_end = d->m_output_buf; + d->m_prev_return_status = TDEFL_STATUS_OKAY; + d->m_saved_match_dist = d->m_saved_match_len = d->m_saved_lit = 0; + d->m_adler32 = 1; + d->m_pIn_buf = NULL; + d->m_pOut_buf = NULL; + d->m_pIn_buf_size = NULL; + d->m_pOut_buf_size = NULL; + d->m_flush = TDEFL_NO_FLUSH; + d->m_pSrc = NULL; + d->m_src_buf_left = 0; + d->m_out_buf_ofs = 0; + if (!(flags & TDEFL_NONDETERMINISTIC_PARSING_FLAG)) + MZ_CLEAR_ARR(d->m_dict); memset(&d->m_huff_count[0][0], 0, sizeof(d->m_huff_count[0][0]) * TDEFL_MAX_HUFF_SYMBOLS_0); memset(&d->m_huff_count[1][0], 0, sizeof(d->m_huff_count[1][0]) * TDEFL_MAX_HUFF_SYMBOLS_1); -#else + return TDEFL_STATUS_OKAY; +} + +tdefl_status tdefl_init_mem(tdefl_compressor *d, void *out, size_t outlen, int flags) +{ tdefl_compressor zero = {0}; *d = zero; // invalidated TDEFL_NONDETERMINISTIC_PARSING_FLAG option here @@ -1570,7 +1598,6 @@ tdefl_status tdefl_init(tdefl_compressor *d, void *out, size_t outlen, int flags d->m_flush = TDEFL_NO_FLUSH; //memset(&d->m_huff_count[0][0], 0, sizeof(d->m_huff_count[0][0]) * TDEFL_MAX_HUFF_SYMBOLS_0); //memset(&d->m_huff_count[1][0], 0, sizeof(d->m_huff_count[1][0]) * TDEFL_MAX_HUFF_SYMBOLS_1); -#endif return TDEFL_STATUS_OKAY; } @@ -1591,7 +1618,7 @@ unsigned deflate_encode(const void *in, unsigned inlen, void *out, unsigned outl mz_uint comp_flags = tdefl_num_probes[level % 12] | (level <= 3 ? TDEFL_GREEDY_PARSING_FLAG : 0);// | TDEFL_WRITE_ZLIB_HEADER ); tdefl_compressor *pComp = (tdefl_compressor*)MZ_REALLOC(0,sizeof(tdefl_compressor)); if (!pComp) return MZ_FALSE; - if(tdefl_init(pComp, out, outlen, (int)comp_flags) == TDEFL_STATUS_OKAY) { + if(tdefl_init_mem(pComp, out, outlen, (int)comp_flags) == TDEFL_STATUS_OKAY) { if(tdefl_compress_buffer(pComp, in, inlen, TDEFL_FINISH) == TDEFL_STATUS_DONE) { bytes = pComp->m_outbuffer[1] - pComp->m_outbuffer[0]; } diff --git a/src/3rd_zlib.h b/src/3rd_zlib.h index 55a1e46..9fca533 100644 --- a/src/3rd_zlib.h +++ b/src/3rd_zlib.h @@ -1,5 +1,24 @@ #pragma once +#define deflate mz_deflate +#define deflateInit2 mz_deflateInit2 +#define inflate mz_inflate +#define inflateInit2 mz_inflateInit2 +#define z_stream mz_stream + +#define Z_ADLER32_INIT MZ_ADLER32_INIT +#define Z_DEFAULT_STRATEGY MZ_DEFAULT_STRATEGY +#define Z_DEFAULT_WINDOW_BITS MZ_DEFAULT_WINDOW_BITS +#define Z_DEFLATED MZ_DEFLATED +#define Z_EMPTY MZ_EMPTY +#define Z_FINISH MZ_FINISH +#define Z_NO_FLUSH MZ_NO_FLUSH +#define Z_NULL MZ_NULL +#define Z_OK MZ_OK +#define Z_RLE MZ_RLE +#define Z_STREAM_END MZ_STREAM_END +#define Z_STREAM_ERROR MZ_STREAM_ERROR + #ifndef MINIZ_EXPORT #define MINIZ_EXPORT #endif @@ -75,6 +94,30 @@ enum MZ_PARAM_ERROR = -10000 }; +enum +{ + MZ_DEFAULT_STRATEGY = 0, + MZ_FILTERED = 1, + MZ_HUFFMAN_ONLY = 2, + MZ_RLE = 3, + MZ_FIXED = 4 +}; + +/* Compression levels: 0-9 are the standard zlib-style levels, 10 is best possible compression (not zlib compatible, and may be very slow), MZ_DEFAULT_COMPRESSION=MZ_DEFAULT_LEVEL. */ +enum +{ + MZ_NO_COMPRESSION = 0, + MZ_BEST_SPEED = 1, + MZ_BEST_COMPRESSION = 9, + MZ_UBER_COMPRESSION = 10, + MZ_DEFAULT_LEVEL = 6, + MZ_DEFAULT_COMPRESSION = -1 +}; + +#define MZ_ADLER32_INIT 1 +#define MZ_DEFLATED 8 +#define MZ_DEFAULT_WINDOW_BITS 15 + typedef void *(*mz_alloc_func)(void *opaque, size_t items, size_t size); typedef void (*mz_free_func)(void *opaque, void *address); typedef void *(*mz_realloc_func)(void *opaque, void *address, size_t items, size_t size); @@ -370,3 +413,153 @@ int mz_uncompress(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char { return mz_uncompress2(pDest, pDest_len, pSource, &source_len); } + +// ---------------------------------------------------------------------------- + +static const mz_uint s_tdefl_num_probes[11] = { 0, 1, 6, 32, 16, 32, 128, 256, 512, 768, 1500 }; + +mz_uint32 tdefl_get_adler32(tdefl_compressor *d) +{ + return d->m_adler32; +} + +/* level may actually range from [0,10] (10 is a "hidden" max level, where we want a bit more compression and it's fine if throughput to fall off a cliff on some files). */ +mz_uint tdefl_create_comp_flags_from_zip_params(int level, int window_bits, int strategy) +{ + mz_uint comp_flags = s_tdefl_num_probes[(level >= 0) ? MZ_MIN(10, level) : MZ_DEFAULT_LEVEL] | ((level <= 3) ? TDEFL_GREEDY_PARSING_FLAG : 0); + if (window_bits > 0) + comp_flags |= TDEFL_WRITE_ZLIB_HEADER; + + if (!level) + comp_flags |= TDEFL_FORCE_ALL_RAW_BLOCKS; + else if (strategy == MZ_FILTERED) + comp_flags |= TDEFL_FILTER_MATCHES; + else if (strategy == MZ_HUFFMAN_ONLY) + comp_flags &= ~TDEFL_MAX_PROBES_MASK; + else if (strategy == MZ_FIXED) + comp_flags |= TDEFL_FORCE_ALL_STATIC_BLOCKS; + else if (strategy == MZ_RLE) + comp_flags |= TDEFL_RLE_MATCHES; + + return comp_flags; +} + +int mz_deflateInit(mz_streamp pStream, int level) +{ + return mz_deflateInit2(pStream, level, MZ_DEFLATED, MZ_DEFAULT_WINDOW_BITS, 9, MZ_DEFAULT_STRATEGY); +} + +int mz_deflateInit2(mz_streamp pStream, int level, int method, int window_bits, int mem_level, int strategy) +{ + tdefl_compressor *pComp; + mz_uint comp_flags = TDEFL_COMPUTE_ADLER32 | tdefl_create_comp_flags_from_zip_params(level, window_bits, strategy); + + if (!pStream) + return MZ_STREAM_ERROR; + if ((method != MZ_DEFLATED) || ((mem_level < 1) || (mem_level > 9)) || ((window_bits != MZ_DEFAULT_WINDOW_BITS) && (-window_bits != MZ_DEFAULT_WINDOW_BITS))) + return MZ_PARAM_ERROR; + + pStream->data_type = 0; + pStream->adler = MZ_ADLER32_INIT; + pStream->msg = NULL; + pStream->reserved = 0; + pStream->total_in = 0; + pStream->total_out = 0; + if (!pStream->zalloc) + pStream->zalloc = miniz_def_alloc_func; + if (!pStream->zfree) + pStream->zfree = miniz_def_free_func; + + pComp = (tdefl_compressor *)pStream->zalloc(pStream->opaque, 1, sizeof(tdefl_compressor)); + if (!pComp) + return MZ_MEM_ERROR; + + pStream->state = (struct mz_internal_state *)pComp; + + if (tdefl_init(pComp, NULL, NULL, comp_flags) != TDEFL_STATUS_OKAY) + { + mz_deflateEnd(pStream); + return MZ_PARAM_ERROR; + } + + return MZ_OK; +} + +int mz_deflateReset(mz_streamp pStream) +{ + if ((!pStream) || (!pStream->state) || (!pStream->zalloc) || (!pStream->zfree)) + return MZ_STREAM_ERROR; + pStream->total_in = pStream->total_out = 0; + tdefl_init((tdefl_compressor *)pStream->state, NULL, NULL, ((tdefl_compressor *)pStream->state)->m_flags); + return MZ_OK; +} + +int mz_deflate(mz_streamp pStream, int flush) +{ + size_t in_bytes, out_bytes; + mz_ulong orig_total_in, orig_total_out; + int mz_status = MZ_OK; + + if ((!pStream) || (!pStream->state) || (flush < 0) || (flush > MZ_FINISH) || (!pStream->next_out)) + return MZ_STREAM_ERROR; + if (!pStream->avail_out) + return MZ_BUF_ERROR; + + if (flush == MZ_PARTIAL_FLUSH) + flush = MZ_SYNC_FLUSH; + + if (((tdefl_compressor *)pStream->state)->m_prev_return_status == TDEFL_STATUS_DONE) + return (flush == MZ_FINISH) ? MZ_STREAM_END : MZ_BUF_ERROR; + + orig_total_in = pStream->total_in; + orig_total_out = pStream->total_out; + for (;;) + { + tdefl_status defl_status; + in_bytes = pStream->avail_in; + out_bytes = pStream->avail_out; + + defl_status = tdefl_compress((tdefl_compressor *)pStream->state, pStream->next_in, &in_bytes, pStream->next_out, &out_bytes, (tdefl_flush)flush); + pStream->next_in += (mz_uint)in_bytes; + pStream->avail_in -= (mz_uint)in_bytes; + pStream->total_in += (mz_uint)in_bytes; + pStream->adler = tdefl_get_adler32((tdefl_compressor *)pStream->state); + + pStream->next_out += (mz_uint)out_bytes; + pStream->avail_out -= (mz_uint)out_bytes; + pStream->total_out += (mz_uint)out_bytes; + + if (defl_status < 0) + { + mz_status = MZ_STREAM_ERROR; + break; + } + else if (defl_status == TDEFL_STATUS_DONE) + { + mz_status = MZ_STREAM_END; + break; + } + else if (!pStream->avail_out) + break; + else if ((!pStream->avail_in) && (flush != MZ_FINISH)) + { + if ((flush) || (pStream->total_in != orig_total_in) || (pStream->total_out != orig_total_out)) + break; + return MZ_BUF_ERROR; /* Can't make forward progress without some input. + */ + } + } + return mz_status; +} + +int mz_deflateEnd(mz_streamp pStream) +{ + if (!pStream) + return MZ_STREAM_ERROR; + if (pStream->state) + { + pStream->zfree(pStream->opaque, pStream->state); + pStream->state = NULL; + } + return MZ_OK; +} diff --git a/src/app.c b/src/app.c index e27c049..c610e03 100644 --- a/src/app.c +++ b/src/app.c @@ -14,7 +14,7 @@ // glue sequential tzx/taps in zips (side A) -> side 1 etc) // sequential tzx/taps/dsks do not reset model -#define SPECTRAL "v1.01" +#define SPECTRAL "v1.02 wip" #define README \ "Spectral can be configured with a mouse.\n\n" \ @@ -353,11 +353,21 @@ void draw_ui() { if( len == 6912 ) memcpy(VRAM, data, len); } } - if( zxdb_url(ZXDB, "instructions") && ui_click(va("- Toggle Instructions -"), "Help\n")) { // @todo: word wrap. mouse panning. rmb close - for( char *data = zxdb_download(ZXDB,zxdb_url(ZXDB, "instructions"), &len); data; free(data), data = 0 ) { - do_overlay ^= 1; - tigrClear(ui, !do_overlay ? tigrRGBA(0,0,0,0) : tigrRGBA(0,0,0,OVERLAY_ALPHA)); - if( do_overlay ) ui_monospaced = 0, ui_print(overlay, 4,4, ui_colors, as_utf8(replace(data, "\t", " "))); + if( zxdb_url(ZXDB, "ay") && ui_click(va("- Toggle Music Tracks -"), "Tunes\n")) { + int scrlen; char *scrdata = zxdb_download(ZXDB,zxdb_url(ZXDB, "screen"), &scrlen); + + // load & play tune + for( char *data = zxdb_download(ZXDB,zxdb_url(ZXDB, "ay"), &len); data; free(data), data = 0 ) { + loadbin(data, len, false); + } + + // use loading screen as a background + if( scrlen == 6912 ) memcpy(VRAM, scrdata, scrlen); + free(scrdata); + } + if( zxdb_url(ZXDB, "mp3") && ui_click(va("- Toggle Bonus Track -"), "Bonus\n")) { + for( char *data = zxdb_download(ZXDB,zxdb_url(ZXDB, "mp3"), &len); data; free(data), data = 0 ) { + // loadbin(data, len, false); } } if( zxdb_url(ZXDB, "map") && ui_click(va("- Toggle Game Map -"), "Maps\n")) { @@ -373,17 +383,18 @@ void draw_ui() { } } } - if( zxdb_url(ZXDB, "mp3") && ui_click(va("- Toggle Bonus Track -"), "Bonus\n")) { - for( char *data = zxdb_download(ZXDB,zxdb_url(ZXDB, "mp3"), &len); data; free(data), data = 0 ) { - // loadbin(data, len, false); - // if( len == 6912 ) memcpy(VRAM, data, len); - } - } if( zxdb_url(ZXDB, "poke") && ui_click("- Cheats -", "Cheats\n") ) { // @todo: selector for( char *data = zxdb_download(ZXDB,zxdb_url(ZXDB, "poke"), &len); data; free(data), data = 0 ) { loadbin(data, len, false); } } + if( zxdb_url(ZXDB, "instructions") && ui_click(va("- Toggle Instructions -"), "Help\n")) { // @todo: word wrap. mouse panning. rmb close + for( char *data = zxdb_download(ZXDB,zxdb_url(ZXDB, "instructions"), &len); data; free(data), data = 0 ) { + do_overlay ^= 1; + tigrClear(ui, !do_overlay ? tigrRGBA(0,0,0,0) : tigrRGBA(0,0,0,OVERLAY_ALPHA)); + if( do_overlay ) ui_monospaced = 0, ui_print(overlay, 4,4, ui_colors, as_utf8(replace(data, "\t", " "))); + } + } const char *roles[] = { ['?'] = "", @@ -1090,7 +1101,7 @@ if( do_runahead == 0 ) { break; case 'TROM': ZX_TURBOROM ^= 1; boot(ZX, 0|KEEP_MEDIA); if(!loadfile(last_load,1)) zxdb_reload(0); // toggle turborom and reload break; case 'F5': reset(0|KEEP_MEDIA); if(!loadfile(last_load, 1)) zxdb_reload(0); - break; case 'NMI': if( pins & Z80_NMI ) pins &= ~Z80_NMI; else pins |= Z80_NMI; // @todo: verify + break; case 'NMI': if( pins & Z80_NMI ) pins &= ~Z80_NMI; else pins |= Z80_NMI; RZX_reset(); // @todo: verify break; case 'BOMB': reset(0); ZXDB = zxdb_free(ZXDB); // clear media KEEP_MEDIA/*|QUICK_RESET*/); // if(last_load) free(last_load), last_load = 0; break; case 'SWAP': // toggle model and reload { diff --git a/src/emu.h b/src/emu.h index 99d9dec..cfa1248 100644 --- a/src/emu.h +++ b/src/emu.h @@ -18,6 +18,8 @@ int play(int sample, unsigned count); // this is from sys headers actually #include "emu_ayumi.h" #include "emu_fdc.h" #include "emu_wd1793.h" +#define block block2 +#include "emu_rzx.h" #if! REDCODE diff --git a/src/res/rzx/rzx.c b/src/emu_rzx.h similarity index 88% rename from src/res/rzx/rzx.c rename to src/emu_rzx.h index 86ce95a..91f2099 100644 --- a/src/res/rzx/rzx.c +++ b/src/emu_rzx.h @@ -18,6 +18,136 @@ - The present licence must be not be removed or altered. */ +#ifndef RZX_LIBRARY_INCLUDE +#define RZX_LIBRARY_INCLUDE + +#include + +#ifdef __cplusplus +extern "C" { +#endif + + +#define RZX_LIBRARY_VERSION 0x000C +#define RZX_LIBRARY_BUILD 31 + + +/* ******************** Configuration options ******************** */ + +/* If needed, please edit the data types definitions as requested */ +typedef uint8_t rzx_u8; /* must be unsigned 8-bit */ +typedef uint16_t rzx_u16; /* must be unsigned 16-bit */ +typedef uint32_t rzx_u32; /* must be unsigned 32-bit */ + +/* Uncomment the next line for Motorola-byte-order CPUs */ +/* #define RZX_BIG_ENDIAN */ + +/* Uncomment the next line to enable compression support */ +#define RZX_USE_COMPRESSION + +/* Uncomment this to enable some exports for debugging */ +#define RZX_DEBUG + +/* *************************************************************** */ + + +/* RZX error codes */ +#define RZX_OK 0 +#define RZX_NOTFOUND -1 +#define RZX_INVALID -2 +#define RZX_FINISHED -3 +#define RZX_UNSUPPORTED -4 +#define RZX_NOMEMORY -5 +#define RZX_SYNCLOST -6 + +/* RZX operation mode */ +#define RZX_IDLE 0 +#define RZX_PLAYBACK 1 +#define RZX_RECORD 2 + + +/* RZX callback messages */ +#define RZXMSG_CREATOR 1 +#define RZXMSG_LOADSNAP 2 +#define RZXMSG_IRBNOTIFY 3 + + +/* RZX global flags */ +#define RZX_PROTECTED 0x0001 +#define RZX_SEALED 0x0002 +#define RZX_REMOVE 0x0004 +#define RZX_EXTERNAL 0x0008 +#define RZX_COMPRESSED 0x0010 + + +/* RZX data types */ +typedef rzx_u32 (*RZX_CALLBACK)(int msg, void *param); + +typedef struct +{ + int mode; + char filename[260]; + rzx_u8 ver_major; + rzx_u8 ver_minor; + rzx_u32 options; +} RZX_INFO; + +typedef struct +{ + char name[20]; + rzx_u16 ver_major; + rzx_u16 ver_minor; + rzx_u8 *data; + rzx_u32 length; + rzx_u32 options; +} RZX_EMULINFO; + +typedef struct +{ + char filename[260]; + rzx_u32 length; + rzx_u32 options; +} RZX_SNAPINFO; + +typedef struct +{ + rzx_u32 framecount; + rzx_u32 tstates; + rzx_u32 options; +} RZX_IRBINFO; + + +/* RZX public data structures */ +extern RZX_INFO rzx; + + +/* RZX functions API */ +int rzx_init(const RZX_EMULINFO *emul, const RZX_CALLBACK callback); +int rzx_record(const char *filename); +int rzx_playback(const char *filename); +void rzx_close(void); +int rzx_update(rzx_u16 *icount); +void rzx_store_input(rzx_u8 value); +rzx_u8 rzx_get_input(void); + +int rzx_add_snapshot(const char *filename, const rzx_u32 flags); +int rzx_add_comment(const char *text, const rzx_u32 flags); + + +#ifdef RZX_DEBUG +extern rzx_u16 INcount; +extern rzx_u16 INmax; +extern rzx_u8 *inputbuffer; +#endif + + +#ifdef __cplusplus +} +#endif +#endif + +// -------------------------------------------------------------------------- + #ifdef __cplusplus extern "C" { #endif @@ -25,14 +155,14 @@ extern "C" { #include #include #include -#include "rzx.h" +//#include "rzx.h" #ifndef _MSC_VER #include #endif #ifdef RZX_USE_COMPRESSION -#include +//#include #endif @@ -521,7 +651,9 @@ void rzx_close(void) switch(rzx.mode) { case RZX_PLAYBACK: +#ifdef RZX_USE_COMPRESSION rzx_pclose(); +#endif break; case RZX_RECORD: /* is there an IRB to close? */ diff --git a/src/emu_z80.h b/src/emu_z80.h index 6bccb96..1aad4f7 100644 --- a/src/emu_z80.h +++ b/src/emu_z80.h @@ -304,6 +304,7 @@ typedef struct { bool prefix_active; // true if any prefix currently active (only needed in z80_opdone()) uint64_t pins; // last pin state, used for NMI detection uint64_t int_bits; // track INT and NMI state + uint64_t fetches; // track number of opcode fetches //< @r-lyeh union { struct { uint8_t pcl; uint8_t pch; }; uint16_t pc; }; // NOTE: These unions are fine in C, but not C++. @@ -1653,6 +1654,7 @@ static inline uint64_t _z80_refresh(z80_t* cpu, uint64_t pins) { // initiate a fetch machine cycle for regular (non-prefixed) instructions, or initiate interrupt handling static inline uint64_t _z80_fetch(z80_t* cpu, uint64_t pins) { + ++cpu->fetches; //< @r-lyeh cpu->hlx_idx = 0; cpu->prefix_active = false; // shortcut no interrupts requested diff --git a/src/res/rzx/LICENSE b/src/res/rzx/LICENSE deleted file mode 100644 index 84bdb44..0000000 --- a/src/res/rzx/LICENSE +++ /dev/null @@ -1,25 +0,0 @@ -BSD 2-Clause License - -Copyright (c) 2022, Kio -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/res/rzx/README.md b/src/res/rzx/README.md deleted file mode 100644 index 84b465b..0000000 --- a/src/res/rzx/README.md +++ /dev/null @@ -1 +0,0 @@ -https://worldofspectrum.net/RZXformat.html diff --git a/src/res/rzx/makefile b/src/res/rzx/makefile deleted file mode 100644 index cc50146..0000000 --- a/src/res/rzx/makefile +++ /dev/null @@ -1,5 +0,0 @@ -C_FLAGS = -O2 -m486 - -test: test.c rzx.c - gcc $(C_FLAGS) -o test test.c rzx.c -lz - diff --git a/src/res/rzx/rzx.h b/src/res/rzx/rzx.h deleted file mode 100644 index 90ab040..0000000 --- a/src/res/rzx/rzx.h +++ /dev/null @@ -1,138 +0,0 @@ -/* - R Z X - - Input Recording Library for ZX Spectrum emulators - ================================================================= - Library version: 0.12 - last updated: 4 August 2002 - - -*/ - - -#ifndef RZX_LIBRARY_INCLUDE -#define RZX_LIBRARY_INCLUDE - -#include "stdint.h" - -#ifdef __cplusplus -extern "C" { -#endif - - -#define RZX_LIBRARY_VERSION 0x000C -#define RZX_LIBRARY_BUILD 31 - - -/* ******************** Configuration options ******************** */ - -/* If needed, please edit the data types definitions as requested */ -typedef uint8_t rzx_u8; /* must be unsigned 8-bit */ -typedef uint16_t rzx_u16; /* must be unsigned 16-bit */ -typedef uint32_t rzx_u32; /* must be unsigned 32-bit */ - -/* Uncomment the next line for Motorola-byte-order CPUs */ -/* #define RZX_BIG_ENDIAN */ - -/* Uncomment the next line to enable compression support */ -#define RZX_USE_COMPRESSION - -/* Uncomment this to enable some exports for debugging */ -#define RZX_DEBUG - -/* *************************************************************** */ - - -/* RZX error codes */ -#define RZX_OK 0 -#define RZX_NOTFOUND -1 -#define RZX_INVALID -2 -#define RZX_FINISHED -3 -#define RZX_UNSUPPORTED -4 -#define RZX_NOMEMORY -5 -#define RZX_SYNCLOST -6 - -/* RZX operation mode */ -#define RZX_IDLE 0 -#define RZX_PLAYBACK 1 -#define RZX_RECORD 2 - - -/* RZX callback messages */ -#define RZXMSG_CREATOR 1 -#define RZXMSG_LOADSNAP 2 -#define RZXMSG_IRBNOTIFY 3 - - -/* RZX global flags */ -#define RZX_PROTECTED 0x0001 -#define RZX_SEALED 0x0002 -#define RZX_REMOVE 0x0004 -#define RZX_EXTERNAL 0x0008 -#define RZX_COMPRESSED 0x0010 - - -/* RZX data types */ -typedef rzx_u32 (*RZX_CALLBACK)(int msg, void *param); - -typedef struct -{ - int mode; - char filename[260]; - rzx_u8 ver_major; - rzx_u8 ver_minor; - rzx_u32 options; -} RZX_INFO; - -typedef struct -{ - char name[20]; - rzx_u16 ver_major; - rzx_u16 ver_minor; - rzx_u8 *data; - rzx_u32 length; - rzx_u32 options; -} RZX_EMULINFO; - -typedef struct -{ - char filename[260]; - rzx_u32 length; - rzx_u32 options; -} RZX_SNAPINFO; - -typedef struct -{ - rzx_u32 framecount; - rzx_u32 tstates; - rzx_u32 options; -} RZX_IRBINFO; - - -/* RZX public data structures */ -extern RZX_INFO rzx; - - -/* RZX functions API */ -int rzx_init(const RZX_EMULINFO *emul, const RZX_CALLBACK callback); -int rzx_record(const char *filename); -int rzx_playback(const char *filename); -void rzx_close(void); -int rzx_update(rzx_u16 *icount); -void rzx_store_input(rzx_u8 value); -rzx_u8 rzx_get_input(void); - -int rzx_add_snapshot(const char *filename, const rzx_u32 flags); -int rzx_add_comment(const char *text, const rzx_u32 flags); - - -#ifdef RZX_DEBUG -extern rzx_u16 INcount; -extern rzx_u16 INmax; -extern rzx_u8 *inputbuffer; -#endif - - -#ifdef __cplusplus -} -#endif -#endif diff --git a/src/res/zxdb/Spectral.db.gz b/src/res/zxdb/Spectral.db.gz index 98d966b..d8ccca8 100644 Binary files a/src/res/zxdb/Spectral.db.gz and b/src/res/zxdb/Spectral.db.gz differ diff --git a/src/res/zxdb/make.bat b/src/res/zxdb/make.bat index 2c5a664..151958e 100644 --- a/src/res/zxdb/make.bat +++ b/src/res/zxdb/make.bat @@ -38,5 +38,5 @@ python -m gzip -d Spectral.db.gz && git add Spectral.db zxdb2txt 0..65535 > Spectral.db && python -m gzip --best Spectral.db && echo Ok! -git diff Spectral.db >> Spectral.db.diff && git add Spectral.db.diff && git rm Spectral.db +git diff Spectral.db >> Spectral.db.diff && git add Spectral.db.diff && git rm Spectral.db -f ) diff --git a/src/res/zxdb/zx_db.h b/src/res/zxdb/zx_db.h index 55d2b4b..7f43113 100644 --- a/src/res/zxdb/zx_db.h +++ b/src/res/zxdb/zx_db.h @@ -181,6 +181,7 @@ void zxdb_add_downloads(zxdb *z, const char **downloads) { 17, // Computer/ZX Interface 2 cartridge ROM image dump 18, // DOCK cartridge ROM image dump 22, // BUGFIX tape image + 23, // Ripped in-game/theme music in AY format 27, // Bonus soundtrack(s) in MP3 format 28, // Instructions 29, // Scanned instructions diff --git a/src/sys_ui.h b/src/sys_ui.h index 60c7374..2c0f2c0 100644 --- a/src/sys_ui.h +++ b/src/sys_ui.h @@ -863,8 +863,8 @@ void ui_notify_draw() { // draw black panel int y2 = y-(theFontH+theFontPaddingH); TPixel transp = { 0,0,0, 192 * smooth }; - tigrFillRect(ui_layer, -1,y2, _320+1,_240, transp); - tigrLine(ui_layer, -1,y2, _320+1,y2, ((TPixel){255,255,255,240*smooth})); + tigrFillRect(ui_layer, -1,y2, _320+2,_240, transp); + tigrLine(ui_layer, -1,y2, _320+2,y2, ((TPixel){255,255,255,240*smooth})); } // text diff --git a/src/sys_zip.h b/src/sys_zip.h index 4b66288..5ed5e44 100644 --- a/src/sys_zip.h +++ b/src/sys_zip.h @@ -1,38 +1,47 @@ // zipfilepath` syntax: "c:/prj/my.zip/mydir/myfile" bool zipme(const char *fullpath, const char *bin, int len) { - const char *filename = strstr(fullpath, ".zip/"); filename += !!filename * 5; - if( !filename ) return false; - const char *zipfile = va("%.*s", (int)(filename - 1 - fullpath), fullpath); + const char *filename = strstr(fullpath, ".zip/"); filename += !!filename * 5; + if( !filename ) return false; + const char *zipfile = va("%.*s", (int)(filename - 1 - fullpath), fullpath); - struct zip *z = zip_open(zipfile, "a+b"); - if( z ) { - unsigned compress_level = 9; - const char *entryname = filename; - const char *comment = ""; - bool ok = zip_append_mem(z, entryname, comment, bin, len, compress_level); - zip_close(z); - return ok; - } + struct zip *z = zip_open(zipfile, "a+b"); + if( z ) { + unsigned compress_level = 9; + const char *entryname = filename; + const char *comment = ""; + bool ok = zip_append_mem(z, entryname, comment, bin, len, compress_level); + zip_close(z); + return ok; + } - return false; + return false; } char *unzip(const char *fullpath, int *len) { // must free() after use - const char *filename = strstr(fullpath, ".zip/"); filename += !!filename * 5; - if( !filename ) return NULL; - const char *zipfile = va("%.*s", (int)(filename - 1 - fullpath), fullpath); + const char *filename = strstr(fullpath, ".zip/"); filename += !!filename * 5; + if( !filename ) return NULL; + const char *zipfile = va("%.*s", (int)(filename - 1 - fullpath), fullpath); - char *bin = 0; - struct zip *z = zip_open(zipfile, "rb"); - if( z ) { - int index = zip_find(z, filename); - if( index >= 0 ) { - bin = zip_extract(z, index); - if(len) *len = zip_size(z, index); - } - zip_close(z); - } + char *bin = 0; + struct zip *z = zip_open(zipfile, "rb"); + if( z ) { + int index = -1; + if( strchr(filename, '*') ) { + for( unsigned i = 0, end = zip_count(z); i < end; ++i ) { + if( strmatchi(zip_name(z,i), filename) ) { + index = i; + } + } + } else { + index = zip_find(z, filename); + } + if( index >= 0 ) { + bin = zip_extract(z, index); + if(len) *len = zip_size(z, index); + } + zip_close(z); + } - return bin; + return bin; } diff --git a/src/zx.h b/src/zx.h index a3b596f..4b320b8 100644 --- a/src/zx.h +++ b/src/zx.h @@ -306,10 +306,10 @@ int medias; void media_reset() { medias = 0; for(int i=0;i<16;++i) media[i].bin = realloc(media[i].bin, media[i].len = media[i].pos = 0); } void media_mount(const byte *bin, int len) { media[medias].bin = memcpy(realloc(media[medias].bin, media[medias].len = len), bin, len), media[medias].pos = 0, medias++; } -enum { ALL_FILES = 0, GAMES_ONLY = 6*4, TAPES_AND_DISKS_ONLY = 9*4, DISKS_ONLY = 13*4 }; +enum { ALL_FILES = 0, GAMES_ONLY = 6*4, TAPES_AND_DISKS_ONLY = 10*4, DISKS_ONLY = 14*4 }; int file_is_supported(const char *filename, int skip) { const char *ext = strrchr(filename ? filename : "", '.'); - return ext && strstri(skip+".gz .zip.rar.pok.scr.ay .rom.sna.z80.tap.tzx.pzx.csw.dsk.img.mgt.trd.fdi.scl.$b.$c.$d.", ext); + return ext && strstri(skip+".gz .zip.rar.pok.scr.ay .rom.sna.z80.rzx.tap.tzx.pzx.csw.dsk.img.mgt.trd.fdi.scl.$b.$c.$d.", ext); } #include "zx_ay.h" @@ -321,12 +321,23 @@ int file_is_supported(const char *filename, int skip) { #include "zx_sna.h" // requires page128, ZXBorderColor #include "zx_ula.h" #include "zx_db.h" +#include "zx_rzx.h" -// 0: cannot load, 1: snapshot loaded, 2: tape loaded, 3: disk loaded +// 0: cannot load, 1: snapshot loaded, 2: tape loaded, 3: disk loaded, 4: ay loaded int loadbin_(const byte *ptr, int size, int preloader) { if(!(ptr && size > 10)) return 0; + // is it a zip? unzip & try to recurse... + if( size > 4 && !memcmp(ptr, "PK\3\4", 4) ) { + for( FILE *fp = fopen(".Spectral.loadbin.zip","wb"); fp; fclose(fp), fp = 0) { + fwrite(ptr, size, 1, fp); + } + int len2; char *ptr2 = unzip(".Spectral.loadbin.zip/*", &len2); + int ret2 = loadbin_(ptr2, len2, preloader); + return unlink(".Spectral.loadbin.zip"), free(ptr2), ret2; + } + if( preloader ) { int model = guess(ptr, size); ZX_PENTAGON = model < 0; model = abs(model); @@ -350,7 +361,7 @@ int loadbin_(const byte *ptr, int size, int preloader) { }; if( load_ay(ptr, (int)size) ) { - return 1; + return 4; } // dsk first @@ -394,6 +405,10 @@ int loadbin_(const byte *ptr, int size, int preloader) { return 2; } + if( RZX_load(ptr, size) ) { + return 1; + } + // headerless fixed-size formats now, sorted by ascending file size. if( scr_load(ptr, size) ) { return 1; @@ -1180,6 +1195,8 @@ void run(unsigned TS) { // hold the INT pin for 32 ticks int_counter = 32; // - (4 - (tick & 3)); // 23? + + RZX_tick(); } // clear INT pin after 32 ticks @@ -1816,10 +1833,21 @@ byte inport_(word port) { } byte inport(word port) { + + if( rzx.mode == RZX_PLAYBACK ) { + rzx_last_port = rzx_get_input(); + if( rzx_last_port >= 0 ) return rzx_last_port; + } + byte v = inport_(port); #if NDEBUG <= 0 logport(port, v, 0); #endif + + if( rzx.mode == RZX_RECORD ) { + rzx_store_input(v); + } + return v; } diff --git a/src/zx_db.h b/src/zx_db.h index 3ec141e..213f392 100644 --- a/src/zx_db.h +++ b/src/zx_db.h @@ -135,6 +135,7 @@ void zxdb_add_downloads(zxdb *z, char **downloads) { 17, // Computer/ZX Interface 2 cartridge ROM image dump 18, // DOCK cartridge ROM image dump 22, // BUGFIX tape image + 23, // Ripped AY 27, // Bonus soundtrack(s) in MP3 format 28, // Instructions 29, // Scanned instructions @@ -446,6 +447,7 @@ char* zxdb_url(const zxdb z, const char *hint) { else if( strstri(hint, "snap")) hint = "|10|S", check_media = 1; else if( strstri(hint, "disk")) hint = "|11|D", check_media = 1; else if( strstri(hint, "rom")) hint = "|17|C", check_media = 1; + else if( strstri(hint, "ay")) hint = "|23|R"; else if( strstri(hint, "mp3")) hint = "|27|B"; else if( strstri(hint, "scanned")) hint = "|29|S"; else if( strstri(hint, "instr")) hint = "|28|I"; diff --git a/src/zx_rzx.h b/src/zx_rzx.h new file mode 100644 index 0000000..4385a17 --- /dev/null +++ b/src/zx_rzx.h @@ -0,0 +1,137 @@ +// rzx ------------------------------------------------------------------------ + +RZX_EMULINFO rzx_info; +rzx_u16 rzx_icount; +rzx_u32 rzx_tstates=555; +uint64_t rzx_counter; +int rzx_last_port; +int rzx_frame; + +rzx_u32 RZX_callback(int msg, void *blob) { + RZX_IRBINFO *irb = (RZX_IRBINFO*)blob; + RZX_SNAPINFO *snap = (RZX_SNAPINFO*)blob; + + /**/ if( msg == RZXMSG_CREATOR ) { + + } + else if( msg == RZXMSG_LOADSNAP ) { + printf("> LOADSNAP: '%s' (%i bytes) %s %s\n", + snap->filename, + (int)snap->length, + snap->options & RZX_EXTERNAL ? "#external" : "#embedded", + snap->options & RZX_COMPRESSED ? "#compressed" : "#uncompressed"); + loadfile(snap->filename, 0); + } + else if( msg == RZXMSG_IRBNOTIFY ) { + rzx_counter = cpu.fetches; + + int frm = irb->framecount; + int tst = irb->tstates; + int opt = irb->options; + + if( rzx.mode == RZX_PLAYBACK ) { + // fetch the IRB info if needed + rzx_tstates = irb->tstates; + printf("> IRB notify: tstates=%i %s\n", (int)rzx_tstates, + irb->options & RZX_COMPRESSED ? "#compressed" : "#uncompressed"); + } + else if( rzx.mode==RZX_RECORD ) { + // fill in the relevant info, i.e. tstates, options + irb->tstates = rzx_tstates; + irb->options = 0; + #ifdef RZX_USE_COMPRESSION + irb->options |= RZX_COMPRESSED; + #endif + printf("> IRB notify: tstates=%i %s\n", (int)rzx_tstates, + irb->options & RZX_COMPRESSED ? "#compressed" : "#uncompressed"); + } + } else { + printf("> MSG #%02X\n", msg); + return RZX_INVALID; + } + + return RZX_OK; +} + +bool RZX_stop() { + // if recording or playback, + // Closes the RZX file. Must be called when the recording is finished. It is called automatically at the end of playback. + if( rzx.mode ) { + rzx_close(); + } + rzx_frame = 0; + rzx_counter = 0; + rzx_last_port = 0; + return 1; +} +void RZX_reset() { + RZX_stop(); +} +bool RZX_play(const char *filename, int rec) { + memset(&rzx_info, 0, sizeof(RZX_EMULINFO)); + strcpy(rzx_info.name, "Spectral"); + rzx_info.ver_major = atoi(SPECTRAL+1); // "v1.02" + rzx_info.ver_minor = atoi(SPECTRAL+3); + if( rzx_init(&rzx_info, RZX_callback) != RZX_OK ) + return 0; + + RZX_reset(); + + if( rec ) { + if( rzx_record(filename) != RZX_OK ) + return alert("Unable to start recording"), 0; + // Store a snapshot into the RZX. + // @todo: z80_save("spectral.$$"); + if( rzx_add_snapshot("spectral.$$", RZX_COMPRESSED) != RZX_OK ) + return alert("Unable to insert snapshot"), 0; + unlink("spectral.$$"); + return 1; + } + else { + if( rzx_playback(filename) != RZX_OK ) + return alert("Unable to start playback"), 0; + return 1; + } + + return 0; +} +bool RZX_load(const void *ptr, int len) { + if( memcmp(ptr, "RZX!", 4) ) return 0; + + bool ok = 0; + for( FILE *fp = fopen(".Spectral.rzx", "wb"); fp; fclose(fp), fp = 0) { + ok = fwrite(ptr, len, 1, fp); + } + ok = ok && RZX_play(".Spectral.rzx", 0); + return unlink(".Spectral.rzx"), ok; +} + +void RZX_tick() { + + if( rzx.mode == RZX_IDLE ) { + return; + } + + if( rzx.mode == RZX_PLAYBACK ) { + rzx_icount = rzx_counter - cpu.fetches; + rzx_counter = cpu.fetches; + } + + if( rzx.mode == RZX_RECORD ) { + printf("frame %04i: rzx_icount=%05i(%04X)\n", rzx_frame, rzx_icount, rzx_icount); + + // In recording mode, it writes the current frame (which is icount instructions long) + // to the RZX file and it is called when an interrupt occurs (or it would occur, if + // maskable interrupts are disabled). + rzx_update(&rzx_icount); + } + if( rzx.mode == RZX_PLAYBACK ) { + // In playback mode, this function is called to start a new frame (the new icount is supplied) + if( rzx_update(&rzx_icount) != RZX_OK ) + return; + + printf("frame %04i: rzx_icount=%05i(%04X)\n", rzx_frame, rzx_icount, rzx_icount); + } + + rzx_frame++; +} diff --git a/src/zx_sna.h b/src/zx_sna.h index 63113fc..088e334 100644 --- a/src/zx_sna.h +++ b/src/zx_sna.h @@ -417,6 +417,9 @@ int guess(const byte *ptr, int size) { // guess required model type for given da // ay first if( size > 0x08 &&(!memcmp(ptr, "ZXAYEMUL", 8) )) return 128; + // rzx + if( size > 0x04 &&(!memcmp(ptr, "RZX!", 4) )) return 128; // @fixme: parse inner .z80/.sna instead + // dsk first if( size > 0x08 &&(!memcmp(ptr, "MV - CPC", 8) || !memcmp(ptr, "EXTENDED", 8)) ) return 300;