Files
open-roms/tools/generate_strings.cc
T

1947 lines
76 KiB
C++

//
// Utility to generate compressed messages and BASIC keywords
//
#include "common.h"
#include <unistd.h>
#include <algorithm>
#include <fstream>
#include <iomanip>
#include <regex>
#include <sstream>
#include <map>
#include <vector>
//
// Command line settings
//
std::string CMD_outFile = "out.s";
std::string CMD_cnfFile = "";
//
// Type definition for strings/keywords to generate
//
typedef struct StringEntry
{
bool enabledSTD; // whether enabled for standard build
bool enabledCRT; // whether enabled for standard build with extra ROM cartridge
bool enabledM65; // whether enabled for Mega 65 build
bool enabledU64; // whether enabled for Ultimate 64 build
bool enabledX16; // whether enabled for Commander X16 build
std::string alias; // alias, for the assembler
std::string string; // string/token
uint8_t abbrevLen = 0; // length of token abbreviation
} StringEntry;
enum class ListType
{
KEYWORDS,
STRINGS_BASIC,
DICTIONARY // for internal use only
};
typedef struct StringEntryList
{
ListType type;
std::string name;
std::vector<StringEntry> list;
} StringEntryList;
typedef std::vector<uint8_t> StringEncoded;
typedef std::vector<StringEncoded> StringEncodedList;
// http://www.classic-games.com/commodore64/cbmtoken.html
// https://www.c64-wiki.com/wiki/BASIC_token
// BASIC keywords - V2 dialect
/*
The URLs here provide examples of other BASIC dialects that include the specified commands
or keywords.
All except VERIFY are known with certainty to be part of other BASIC dialects, and thus
cannot be subject to copyright infringement when implemented on a C64.
The VERIFY Command however appears in other programming languages, and as a single word,
is the obvious complement to LOAD and SAVE when considering the following storage functions:
1. Load a file from storage into memory.
2. Save a file from memory to storage.
3. Verify that a file has been correctly written from memory to storage.
Note that verify is the key verb in the behaviour of this function. Therefore it is the
obvious choice for the name for such a function, taking into account the convention of
LOAD and SAVE. Indeed, that this is so is highlighted by the fact that it was used in the
C64 ROM. Therefore there is no exercise of creativity here, and a reasonable person would
likely select this name independently. Also, single words cannot be copyright.
Further, the list is a list of facts, and thus not copyrightable.
We sourced the list from a public source, https://www.c64-wiki.com/wiki/BASIC, which has
been public on the internet for a long time, as have many other similar sources.
That website offers the page under the GFDL license, and we can therefore take the list
under those license terms if we wish, should the need arise.
Note that in providing these justifications above, we do not in any way acknowledge that
without these conditions that the BASIC keyword list could be copyright, but provide this
information as a further layer of defence against any claims that it could somehow
infringe on the copyrights of the C64/C65/C128 etc ROMs.
*/
const StringEntryList GLOBAL_Keywords_V2 = { ListType::KEYWORDS, "keywords_V2",
{
// STD CRT M65 U64 X16
{ true, true, true, true, true, "KV2_80", "END", 2 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_81", "FOR", 2 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_82", "NEXT", 2 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_83", "DATA", 2 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_84", "INPUT#", 2 }, // https://www.atariarchives.org/creativeatari/Using_Disks_With_Atari_Basic.php
{ true, true, true, true, true, "KV2_85", "INPUT", 0 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_86", "DIM", 2 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_87", "READ", 2 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_88", "LET", 2 }, // https://en.wikipedia.org/wiki/Atari_BASIC
{ true, true, true, true, true, "KV2_89", "GOTO", 2 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_8A", "RUN", 2 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_8B", "IF", 0 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_8C", "RESTORE", 3 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_8D", "GOSUB", 3 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_8E", "RETURN", 3 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_8F", "REM", 0 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_90", "STOP", 2 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_91", "ON", 0 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_92", "WAIT", 2 }, // http://www.picaxe.com/BASIC-Commands/Time-Delays/wait/
{ true, true, true, true, true, "KV2_93", "LOAD", 2 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_94", "SAVE", 2 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_95", "VERIFY", 2 }, // https://en.wikipedia.org/wiki/Sinclair_BASIC
{ true, true, true, true, true, "KV2_96", "DEF", 2 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_97", "POKE", 2 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_98", "PRINT#", 2 }, // https://www.atariarchives.org/creativeatari/Using_Disks_With_Atari_Basic.php
{ true, true, true, true, true, "KV2_99", "PRINT", 0 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_9A", "CONT", 2 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_9B", "LIST", 2 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_9C", "CLR", 2 }, // Apple I Replica Creation: Back to the Garage, p125
{ true, true, true, true, true, "KV2_9D", "CMD", 2 }, // https://en.wikipedia.org/wiki/List_of_DOS_commands
{ true, true, true, true, true, "KV2_9E", "SYS", 2 }, // https://www.lifewire.com/dos-commands-4070427
{ true, true, true, true, true, "KV2_9F", "OPEN", 2 }, // https://www.atariarchives.org/creativeatari/Using_Disks_With_Atari_Basic.php
// STD CRT M65 U64 X16
{ true, true, true, true, true, "KV2_A0", "CLOSE", 3 }, // https://www.atariarchives.org/creativeatari/Using_Disks_With_Atari_Basic.php
{ true, true, true, true, true, "KV2_A1", "GET", 2 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_A2", "NEW", 0 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_A3", "TAB(", 2 }, // http://www.antonis.de/qbebooks/gwbasman/tab.html
{ true, true, true, true, true, "KV2_A4", "TO", 0 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_A5", "FN", 0 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_A6", "SPC(", 2 }, // http://www.antonis.de/qbebooks/gwbasman/spc.html
{ true, true, true, true, true, "KV2_A7", "THEN", 2 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_A8", "NOT", 2 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_A9", "STEP", 2 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_AA", "+", 0 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_AB", "-", 0 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_AC", "*", 0 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_AD", "/", 0 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_AE", "^", 0 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_AF", "AND", 2 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_B0", "OR", 0 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_B1", ">", 0 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_B2", "=", 0 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_B3", "<", 0 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_B4", "SGN", 2 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_B5", "INT", 0 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_B6", "ABS", 2 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_B7", "USR", 2 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_B8", "FRE", 2 }, // http://www.antonis.de/qbebooks/gwbasman/fre.html
{ true, true, true, true, true, "KV2_B9", "POS", 0 }, // http://www.antonis.de/qbebooks/gwbasman/pos.html
{ true, true, true, true, true, "KV2_BA", "SQR", 2 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_BB", "RND", 2 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_BC", "LOG", 0 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_BD", "EXP", 2 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_BE", "COS", 0 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_BF", "SIN", 2 }, // https://www.landsnail.com/a2ref.htm
// STD CRT M65 U64 X16
{ true, true, true, true, true, "KV2_C0", "TAN", 0 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_C1", "ATN", 2 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_C2", "PEEK", 2 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_C3", "LEN", 0 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_C4", "STR$", 3 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_C5", "VAL", 2 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_C6", "ASC", 2 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_C7", "CHR$", 2 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_C8", "LEFT$", 3 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_C9", "RIGHT$", 2 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_CA", "MID$", 2 }, // https://www.landsnail.com/a2ref.htm
{ true, true, true, true, true, "KV2_CB", "GO", 0 }, // https://en.wikipedia.org/wiki/Goto
} };
// extended BASIC keywords - list reserved for small BASIC commands, suitable for inclusion in all machines with extended ROM
const StringEntryList GLOBAL_Keywords_01 = { ListType::KEYWORDS, "keywords_01",
{
// STD CRT M65 U64 X16
{ true, true, true, true, true, "K01_01", "SLOW", }, // https://en.wikipedia.org/wiki/Sinclair_BASIC - ZX81 variant
{ true, true, true, true, true, "K01_02", "FAST", }, // https://en.wikipedia.org/wiki/Sinclair_BASIC - ZX81 variant
{ true, true, true, true, true, "K01_03", "OLD", }, // Not present in CBM BASIC dialects, but common to some extensions (*)
{ false, true, true, true, false, "K01_04", "CLEAR", }, // Not present in CBM BASIC dialects, Open ROMs specific
{ false, true, true, true, false, "K01_05", "DISPOSE", }, // Not present in CBM BASIC dialects, Open ROMs specific
{ false, true, true, true, false, "K01_06", "MERGE", }, // Not present in CBM BASIC dialects, but common to some extensions (*)
{ false, true, true, true, false, "K01_07", "BLOAD", }, // http://www.antonis.de/qbebooks/gwbasman/bload.html
{ false, true, true, true, false, "K01_08", "BSAVE", }, // http://www.antonis.de/qbebooks/gwbasman/bsave.html
{ false, true, true, true, false, "K01_09", "BVERIFY", },
{ false, true, true, true, false, "K01_0A", "COLD", }, // Not present in CBM BASIC dialects, but common to some extensions (*)
{ false, true, true, true, false, "K01_0B", "MEM", }, // Not present in CBM BASIC dialects, Open ROMs specific
// (*) see https://sourceforge.net/p/vice-emu/code/HEAD/tree/trunk/vice/src/petcat.c
} };
// extended BASIC keywords - list reserved for hardware dependent BASIC commands
const StringEntryList GLOBAL_Keywords_04 = { ListType::KEYWORDS, "keywords_04",
{
// STD CRT M65 U64 X16
{ false, false, true, false, false, "K04_01", "SYSINFO", }, // Not present in CBM BASIC dialects, Open ROMs specific
{ false, false, true, false, false, "K04_02", "MONITOR", }, // Amos Professional Manual, Command Index
{ false, false, true, false, false, "K04_03", "BOOTCPM", }, // Not present in CBM BASIC dialects, Open ROMs specific
{ false, false, true, false, false, "K04_04", "JOYCRSR", }, // Not present in CBM BASIC dialects, Open ROMs specific
{ false, false, true, false, false, "K04_05", "COLORSET", }, // Not present in CBM BASIC dialects, Open ROMs specific
} };
// extended BASIC keywords - list reserved for functions
const StringEntryList GLOBAL_Keywords_06 = { ListType::KEYWORDS, "keywords_06",
{
// STD CRT M65 U64 X16
{ false, false, true, false, false, "K06_01", "TEST", }, // Not present in CBM BASIC dialects, Open ROMs specific
} };
// BASIC errors - all dialects
/*
These error messages are generally so well known, that it seems silly to have to even point out that they are
so widely used and known, if not completely genericised.
The most of them are inherited from the MICROSOFT BASIC on which the C64's BASIC is based, so for those, there
cannot even be any claim to copyright over them in any exclusive way.
However, copyright of such short phrases that express ideas in short form cannot be copyrighted:
https://fairuse.stanford.edu/2003/09/09/copyright_protection_for_short/
*/
const StringEntryList GLOBAL_Errors = { ListType::STRINGS_BASIC, "errors",
{
// STD CRT M65 U64 X16 --- error strings compatible with CBM BASIC V2
{ true, true, true, true, true, "EV2_01", "TOO MANY FILES" },
{ true, true, true, true, true, "EV2_02", "FILE OPEN" },
{ true, true, true, true, true, "EV2_03", "FILE NOT OPEN" },
{ true, true, true, true, true, "EV2_04", "FILE NOT FOUND" },
{ true, true, true, true, true, "EV2_05", "DEVICE NOT PRESENT" },
{ true, true, true, true, true, "EV2_06", "NOT INPUT FILE" },
{ true, true, true, true, true, "EV2_07", "NOT OUTPUT FILE" },
{ true, true, true, true, true, "EV2_08", "MISSING FILENAME" },
{ true, true, true, true, true, "EV2_09", "ILLEGAL DEVICE NUMBER" },
{ true, true, true, true, true, "EV2_0A", "NEXT WITHOUT FOR" },
{ true, true, true, true, true, "EV2_0B", "SYNTAX" },
{ true, true, true, true, true, "EV2_0C", "RETURN WITHOUT GOSUB" },
{ true, true, true, true, true, "EV2_0D", "OUT OF DATA" },
{ true, true, true, true, true, "EV2_0E", "ILLEGAL QUANTITY" },
{ true, true, true, true, true, "EV2_0F", "OVERFLOW" },
{ true, true, true, true, true, "EV2_10", "OUT OF MEMORY" },
{ true, true, true, true, true, "EV2_11", "UNDEF\'D STATEMENT" },
{ true, true, true, true, true, "EV2_12", "BAD SUBSCRIPT" },
{ true, true, true, true, true, "EV2_13", "REDIM\'D ARRAY" },
{ true, true, true, true, true, "EV2_14", "DIVISION BY ZERO" },
{ true, true, true, true, true, "EV2_15", "ILLEGAL DIRECT" },
{ true, true, true, true, true, "EV2_16", "TYPE MISMATCH" },
{ true, true, true, true, true, "EV2_17", "STRING TOO LONG" },
{ true, true, true, true, true, "EV2_18", "FILE DATA" },
{ true, true, true, true, true, "EV2_19", "FORMULA TOO COMPLEX" },
{ true, true, true, true, true, "EV2_1A", "CAN\'T CONTINUE" },
{ true, true, true, true, true, "EV2_1B", "UNDEF\'D FUNCTION" },
{ true, true, true, true, true, "EV2_1C", "VERIFY" },
{ true, true, true, true, true, "EV2_1D", "LOAD" },
{ true, true, true, true, true, "EV2_1E", "BREAK" },
// STD CRT M65 U64 X16 --- error strings compatible with CBM BASIC V7
{ false, false, false, false, false, "EV7_1F", "CAN'T RESUME" }, // not used for now
{ false, false, false, false, false, "EV7_20", "LOOP NOT FOUND" }, // not used for now
{ false, false, false, false, false, "EV7_21", "LOOP WITHOUT DO" }, // not used for now
{ true, true, true, true, true, "EV7_22", "DIRECT MODE ONLY" },
{ false, false, false, false, false, "EV7_23", "NO GRAPHICS AREA" }, // not used for now
{ false, false, false, false, false, "EV7_24", "BAD DISK" }, // not used for now
{ false, false, false, false, false, "EV7_25", "BEND NOT FOUND" }, // not used for now
{ true, true, true, true, true, "EV7_26", "LINE NUMBER TOO LARGE" },
{ false, false, false, false, false, "EV7_27", "UNRESOLVED REFERENCE" }, // not used for now
{ true, true, true, true, true, "EV7_28", "NOT IMPLEMENTED" }, // this message actually differs from the CBM one
{ false, false, false, false, false, "EV7_29", "FILE READ" }, // not used for now
// STD CRT M65 U64 X16 --- error strings specific to Open ROMs, not present in CBM BASIC dialects
{ true, true, true, true, true, "EOR_2A", "MEMORY CORRUPT" },
{ false, false, true, false, false, "EOR_2B", "NATIVE MODE ONLY" },
{ false, false, true, false, false, "EOR_2C", "LEGACY MODE ONLY" },
} };
// BASIC errors - miscelaneous strings
const StringEntryList GLOBAL_MiscStrings = { ListType::STRINGS_BASIC, "misc",
{
// STD CRT M65 U64 X16 --- misc strings as on CBM machines
{ true, true, true, true, true, "STR_RET_QM", "\r?" },
{ true, true, true, true, true, "STR_BYTES", " BASIC BYTES FREE" }, // https://github.com/stefanhaustein/expressionparser
{ true, true, true, true, true, "STR_READY", "\rREADY.\r" }, // https://www.ibm.com/support/knowledgecenter/zosbasics/com.ibm.zos.zconcepts/zconc_whatistsonative.htm https://github.com/stefanhaustein/expressionparser
{ true, true, true, true, true, "STR_ERROR", " ERROR" }, // simply the word error that is attached to the other parts of messages https://fjkraan.home.xs4all.nl/comp/apple2faq/app2asoftfaq.html
{ true, true, true, true, true, "STR_IN", " IN " },
{ false, true, true, false, false, "STR_IF_SURE", "\rARE YOU SURE? " }, // https://docs.microsoft.com/en-us/windows/win32/uxguide/mess-confirm
// STD CRT M65 U64 X16 --- misc strings specific to Open ROMs, not present in CBM ROMs
{ true, true, true, true, true, "STR_BRK_AT", "\rBRK AT $" },
{ false, false, true, false, false, "STR_ORS", "OPEN ROMS BASIC & KERNAL\r" },
{ false, false, true, false, false, "STR_ORS_LEGACY", "(LEGACY MODE) " },
{ false, true, true, false, false, "STR_MEM_HDR", "\r\x12 AREA START SIZE \r" },
{ false, true, true, false, false, "STR_MEM_1", " $" },
{ false, true, true, false, false, "STR_MEM_2", " " },
{ false, true, true, false, false, "STR_MEM_TEXT", "\r TEXT" },
{ false, true, true, false, false, "STR_MEM_VARS", "\r VARS" },
{ false, true, true, false, false, "STR_MEM_ARRS", "\r ARRS" },
{ false, true, true, false, false, "STR_MEM_STRS", "\r STRS" },
{ false, true, true, false, false, "STR_MEM_FREE", "\r\r FREE" },
{ false, false, true, false, false, "STR_SI_HEADER", "OPEN ROMS, " },
{ false, false, true, false, false, "STR_SI_MODE64", "LEGACY MODE" },
{ false, false, true, false, false, "STR_SI_MODE65", "NATIVE MODE" },
{ false, false, true, false, false, "STR_SI_HDR_REL", "\rRELEASE " },
{ false, false, true, false, false, "STR_SI_HDR_HW", "BOARD : " },
{ false, false, true, false, false, "STR_SI_HW_01", "MEGA65 R1" },
{ false, false, true, false, false, "STR_SI_HW_02", "MEGA65 R2" },
{ false, false, true, false, false, "STR_SI_HW_03", "MEGA65 R3" },
{ false, false, true, false, false, "STR_SI_HW_21", "MEGAPHONE R1" },
{ false, false, true, false, false, "STR_SI_HW_40", "NEXYS4 PSRAM" },
{ false, false, true, false, false, "STR_SI_HW_41", "NEXYS4 DDR" },
{ false, false, true, false, false, "STR_SI_HW_42", "NEXYS4 DDR WIDGET" },
{ false, false, true, false, false, "STR_SI_HW_FD", "WUKONG A100T" },
{ false, false, true, false, false, "STR_SI_HW_FE", "SIMULATION VHDL" },
{ false, false, true, false, false, "STR_SI_HW_XX", "UNKNOWN ID $" },
{ false, false, true, false, false, "STR_SI_HDR_VID", "\rVIDEO : " },
// Note: depending on configuration, additional strings will be added here
} };
/* NOTE: below are the BASIC V7/V10 keywords, see:
- https://sites.google.com/site/h2obsession/CBM/basic/tokens
- http://www.zimmers.net/anonftp/pub/cbm/programming/cbm-basic-tokens.txt
"RGR" // $CC
"RCLR" // $CD
"JOY" // $CF // https://www.qsl.net/hb9xch/computer/amstrad/locomotivebasic.html
"RDOT" // $D0
"DEC" // $D1 // Amos Professional Manual, Command Index
"HEX$" // $D2 // http://www.antonis.de/qbebooks/gwbasman/hexs.html
"ERR$" // $D3 // Amos Professional Manual, Command Index
"INSTR" // $D4 // Amos Professional Manual, Command Index
"ELSE" // $D5 // Amos Professional Manual, Command Index
"RESUME" // $D6 // Amos Professional Manual, Command Index
"TRAP" // $D7 // Amos Professional Manual, Command Index
"TRON" // $D8 // https://www.qsl.net/hb9xch/computer/amstrad/locomotivebasic.html
"TROFF" // $D9 // https://www.qsl.net/hb9xch/computer/amstrad/locomotivebasic.html
"SOUND" // $DA // https://www.qsl.net/hb9xch/computer/amstrad/locomotivebasic.html
"VOL" // $DB
"AUTO" // $DC // http://www.antonis.de/qbebooks/gwbasman/auto.html
"PUDEF" // $DD
"GRAPHIC" // $DE
"PAINT" // $DF // Amos Professional Manual, Command Index
"CHAR" // $E0 // https://www.c64-wiki.com/wiki/Screen_Graphics_64
"BOX" // $E1 // Amos Professional Manual, Command Index
"CIRCLE" // $E2 // https://en.wikipedia.org/wiki/Sinclair_BASIC
"PASTE" // $E3 // Amos Professional Manual, Command Index
"CUT" // $E4
"LINE" // $E5 // https://en.wikipedia.org/wiki/Sinclair_BASIC
"LOCATE" // $E6 // https://www.qsl.net/hb9xch/computer/amstrad/locomotivebasic.html
"COLOR" // $E7
"SCNCLR" // $E8
"SCALE" // $E9 // https://www.c64-wiki.com/wiki/Super_Expander_64
"HELP" // $EA // http://www.classic-games.com/commodore64/cbmtoken.html, Speech Basic 2.7
"DO" // $EB // Amos Professional Manual, Command Index
"LOOP" // $EC // Amos Professional Manual, Command Index
"EXIT" // $ED // Amos Professional Manual, Command Index
"DIR" // $EE // Amos Professional Manual, Command Index
"DSAVE" // $EF // http://www.classic-games.com/commodore64/cbmtoken.html, AtBasic (@Basic)
"DLOAD" // $F0 // http://www.classic-games.com/commodore64/cbmtoken.html, AtBasic (@Basic)
"HEADER" // $F1 // http://www.classic-games.com/commodore64/cbmtoken.html, AtBasic (@Basic)
"SCRATCH" // $F2 // http://www.classic-games.com/commodore64/cbmtoken.html, AtBasic (@Basic)
"COLLECT" // $F3
"COPY" // $F4 // https://en.wikipedia.org/wiki/Sinclair_BASIC
"RENAME" // $F5 // Amos Professional Manual, Command Index
"BACKUP" // $F6 // http://www.classic-games.com/commodore64/cbmtoken.html, AtBasic (@Basic)
"DELETE" // $F7 // http://www.classic-games.com/commodore64/cbmtoken.html, AtBasic (@Basic)
"RENUMBER" // $F8 // http://www.classic-games.com/commodore64/cbmtoken.html, AtBasic (@Basic)
"KEY" // $F9 // https://www.qsl.net/hb9xch/computer/amstrad/locomotivebasic.html
"MONITOR" // $FA // Amos Professional Manual, Command Index
"USING" // $FB // http://www.antonis.de/qbebooks/gwbasman/printusing.html
"UNTIL" // $FC // Amos Professional Manual, Command Index
"WHILE" // $FD // https://www.qsl.net/hb9xch/computer/amstrad/locomotivebasic.html
"POT" // $CE $02
"BUMP" // $CE $03
"PEN" // $CE $04 // https://www.qsl.net/hb9xch/computer/amstrad/locomotivebasic.html
"RSPPOS" // $CE $05 // https://www.c64-wiki.com/wiki/Super_Expander_64
"RSPRITE" // $CE $06
"RSPCOLOR" // $CE $07
"XOR" // $CE $08
"RWINDOW" // $CE $09
"POINTER" // $CE $0A
"BANK" // $FE $02 // Amos Professional Manual, Command Index
"FILTER" // $FE $03 // https://www.c64-wiki.com/wiki/Super_Expander_64
"PLAY" // $FE $04 // Amos Professional Manual, Command Index
"TEMPO" // $FE $05 // Amos Professional Manual, Command Index
"MOVSPR" // $FE $06
"SPRITE" // $FE $07 // Amos Professional Manual, Command Index
"SPRCOLOR" // $FE $08
"RREG" // $FE $09
"ENVELOPE" // $FE $0A
"SLEEP" // $FE $0B
"CATALOG" // $FE $0C // http://www.classic-games.com/commodore64/cbmtoken.html, AtBasic (@Basic)
"DOPEN" // $FE $0D
"APPEND" // $FE $0E // Amos Professional Manual, Command Index
"DCLOSE" // $FE $0F
"BSAVE" // $FE $10 // http://www.antonis.de/qbebooks/gwbasman/bsave.html
"BLOAD" // $FE $11 // http://www.antonis.de/qbebooks/gwbasman/bload.html
"RECORD" // $FE $12
"CONCAT" // $FE $13
"DVERIFY" // $FE $14 // http://www.classic-games.com/commodore64/cbmtoken.html, AtBasic (@Basic)
"DCLEAR" // $FE $15
"SPRSAV" // $FE $16 // https://www.c64-wiki.com/wiki/Super_Expander_64
"COLLISION" // $FE $17
"BEGIN" // $FE $18
"BEND" // $FE $19
"WINDOW" // $FE $1A // http://www.antonis.de/qbebooks/gwbasman/window.html
"BOOT" // $FE $1B
"WIDTH" // $FE $1C // http://www.antonis.de/qbebooks/gwbasman/width.html
"SPRDEF" // $FE $1D // https://www.c64-wiki.com/wiki/Super_Expander_64
"QUIT" // $FE $1E // http://www.classic-games.com/commodore64/cbmtoken.html, Speech Basic 2.7
"STASH" // $FE $1F - v7
"FETCH" // $FE $21 - v7
"SWAP" // $FE $23 - v7 // http://www.antonis.de/qbebooks/gwbasman/swap.html
"DMA" // $FE $17/$21/$23 - v10
"OFF" // $FE $24 // http://www.classic-games.com/commodore64/cbmtoken.html, Speech Basic 2.7
"FAST" // $FE $25 // https://en.wikipedia.org/wiki/Sinclair_BASIC - ZX81 variant
"SLOW" // $FE $26 // https://en.wikipedia.org/wiki/Sinclair_BASIC - ZX81 variant
"TYPE" // $FE $27
"BVERIFY" // $FE $28
"DIRECTORY" // $FE $29 // http://www.classic-games.com/commodore64/cbmtoken.html, AtBasic (@Basic)
"ERASE" // $FE $2A // https://en.wikipedia.org/wiki/Sinclair_BASIC
"FIND" // $FE $2B // http://www.classic-games.com/commodore64/cbmtoken.html, AtBasic (@Basic)
"CHANGE" // $FE $2C
"SET" // $FE $2D
"SCREEN" // $FE $2E // http://www.antonis.de/qbebooks/gwbasman/screens.html
"POLYGON" // $FE $2F // Amos Professional Manual, Command Index
"ELLIPSE" // $FE $30 // Amos Professional Manual, Command Index
"VIEWPORT" // $FE $31
"GCOPY" // $FE $32
"PEN" // $FE $33 // Amos Professional Manual, Command Index
"PALETTE" // $FE $34 // Amos Professional Manual, Command Index
"DMODE" // $FE $35
"DPAT" // $FE $36
"PIC" // $FE $37
"GENLOCK" // $FE $38
"FOREGROUND" // $FE $39
"BACKGROUND" // $FE $3B
"BORDER" // $FE $3C // https://en.wikipedia.org/wiki/Sinclair_BASIC
"HIGHLIGHT" // $FE $3D
*/
std::map<std::string, bool> GLOBAL_ConfigOptions;
//
// Work class definitions
//
// This class encapsulates the optional dictionary encoding of selected string lists
class DictEncoder
{
public:
void addString(const std::string &inString, StringEncoded *outPtr);
void process(StringEntryList &outDictionary);
private:
bool optimizeSplit();
bool optimizeJoin();
void optimizeOrder();
void cleanupDictionary();
void extractWords(std::vector<std::string> &candidateList);
int32_t evaluateCandidate(std::string &candidate);
std::vector<StringEncoded *> encodings;
std::vector<std::string> dictionary;
};
// Main class to encode strings based on character frequency
class DataSet
{
public:
void addStrings(const StringEntryList &stringList);
const std::string &getOutput();
private:
void process();
void generateConfigDepStrings();
void validateLists();
void calculateFrequencies();
void encodeStringsDict();
void encodeStringsFreq();
void encodeByFreq(const std::string &plain, StringEncoded &encoded) const;
void prepareOutput();
void prepareOutput_1n_3n(std::ostringstream &stream);
void prepareOutput_labels(std::ostringstream &stream,
const StringEntryList &stringEntryList,
const StringEncodedList &stringEncodedList);
void prepareOutput_packed(std::ostringstream &stream,
const StringEntryList &stringEntryList,
const StringEncodedList &stringEncodedList);
void putCharEncoding(std::ostringstream &stream, uint8_t idx, char character, bool is3n);
bool isCompressionLvl2(const StringEntryList &list) const;
virtual bool isRelevant(const StringEntry &entry) const = 0;
virtual std::string layoutName() const = 0;
std::vector<StringEntryList> stringEntryLists;
std::vector<StringEncodedList> stringEncodedLists;
std::vector<char> as1n; // list of bytes to be encoded as 1 nibble
std::vector<char> as3n; // list of bytes to be encoded as 3 nibbles
uint8_t tk__packed_as_3n = 0;
uint8_t tk__max_keyword_len = 0;
size_t maxAliasLen = 0;
std::string outFileContent;
};
class DataSetSTD : public DataSet
{
bool isRelevant(const StringEntry &entry) const { return entry.enabledSTD; }
std::string layoutName() const { return "STD"; }
};
class DataSetCRT : public DataSet
{
bool isRelevant(const StringEntry &entry) const { return entry.enabledCRT; }
std::string layoutName() const { return "CRT"; }
};
class DataSetM65 : public DataSet
{
bool isRelevant(const StringEntry &entry) const { return entry.enabledM65; }
std::string layoutName() const { return "M65"; }
};
class DataSetU64 : public DataSet
{
bool isRelevant(const StringEntry &entry) const { return entry.enabledU64; }
std::string layoutName() const { return "STD"; } // XXX to be changed for 'U64' once the layout is finished
};
class DataSetX16 : public DataSet
{
bool isRelevant(const StringEntry &entry) const { return entry.enabledX16; }
std::string layoutName() const { return "X16"; }
};
//
// Work class implementation
//
void DictEncoder::addString(const std::string &inString, StringEncoded *outPtr)
{
// Store the pointer to encoding
encodings.push_back(outPtr);
// Check if string is already present in the dictionary
auto pos = std::find(dictionary.begin(), dictionary.end(), inString);
// Create initial encoding
if (pos != dictionary.end())
{
encodings.back()->push_back(pos - dictionary.begin());
}
else
{
if (dictionary.size() > 255)
{
ERROR("max 255 strings allowed for dictionary compression");
}
dictionary.push_back(inString);
encodings.back()->push_back(dictionary.size() - 1);
}
}
void DictEncoder::extractWords(std::vector<std::string> &candidateList)
{
for (const auto &dictionaryEntry : dictionary)
{
// Split the dictionary entry into words
std::istringstream entryStream(dictionaryEntry);
while (entryStream)
{
std::string word_1;
entryStream >> word_1;
if (word_1.empty()) continue;
// Create possible variants with spaces
std::string word_2 = " " + word_1;
std::string word_3 = word_1 + " ";
std::string word_4 = word_2 + " ";
// If necessary, add the word to candidate list
auto addToList = [&dictionaryEntry, &candidateList](std::string &word)
{
// Before dding, make sure the word variant exists in the string,
// is not already on the list, and is long enough it makes sense to try
if (word.size() < 1) return;
if (dictionaryEntry.find(word) == std::string::npos) return;
if (std::find(candidateList.begin(), candidateList.end(), word) != candidateList.end()) return;
candidateList.push_back(word);
};
addToList(word_1);
addToList(word_2);
addToList(word_3);
addToList(word_4);
}
}
}
int32_t DictEncoder::evaluateCandidate(std::string &candidate)
{
// XXX it looks like this is not 100% right - debug the algorithm if dictionary compression is reintroduced
// Build the score - find how much bytes can be spared by extracting this particular candidate
// Limited size of the dictionary is taken into consideration
int32_t score = -(candidate.size() + 1);
uint32_t targetSize = dictionary.size() + 1;
// First check for situation when the candidate equals the currently existing dictionary entry
for (const auto &dictionaryEntry : dictionary)
{
if (dictionaryEntry == candidate)
{
// Candidate equals the string
score += candidate.size() + 1;
targetSize--;
continue;
}
}
// Now check the remaining cases
for (const auto &dictionaryEntry : dictionary)
{
if (dictionaryEntry == candidate) continue; // already handled
// Find all the occurences of the candidate in the current string
std::vector<uint8_t> occurences;
for (auto iter = dictionaryEntry.begin(); iter < (dictionaryEntry.end() - candidate.size() + 1); iter++)
{
if (candidate != std::string(iter, iter + candidate.size())) continue;
occurences.push_back(iter - dictionaryEntry.begin());
iter += candidate.size() - 1; // to make sure we have no occurence overlapping
}
if (occurences.empty()) continue;
// Now summarize what we are going to gain when doing replacement
if (occurences[0] != 0)
{
// For the substring before the first occurence we need to create a separate entry
// in the dictionary - unless it is already there, it brings some additional cost
std::string otherStr = std::string(dictionaryEntry.begin(), dictionaryEntry.end() + occurences[0]);
if (std::find(dictionary.begin(), dictionary.end(), otherStr) != dictionary.end())
{
// This extra string is already in the dictionary - that gives extra saving
score += otherStr.size() - 1;
}
else
{
// This extra string is not in the dictionary, it has to be added
score -= 2;
targetSize++;
}
}
for (auto iter = occurences.begin(); iter < occurences.end(); iter++)
{
// With a replacement, we should gain some bytes
score += candidate.size() - 1;
// If the occurence ends with the last byte of dictionary string - nothing more to do
if (*iter + candidate.size() == dictionaryEntry.size()) break;
// If next occurence starts immediately after this one - next iteration
if (iter + 1 != occurences.end() && (*(iter + 1) - *iter) == (uint8_t) candidate.size()) continue;
// For the substring between this occurence and the next one (or the end of the string)
// we need to create a separate entry in the dictionary - unless it is already there,
// it brings some additional cost
std::string otherStr;
if (iter + 1 == occurences.end())
{
otherStr = std::string(dictionaryEntry.begin() + *iter + candidate.size(),
dictionaryEntry.end());
}
else
{
otherStr = std::string(dictionaryEntry.begin() + *iter + candidate.size(),
dictionaryEntry.begin() + *(iter + 1));
}
if (std::find(dictionary.begin(), dictionary.end(), otherStr) != dictionary.end())
{
// This extra string is already in the dictionary - that gives extra saving
score += otherStr.size() - 1;
}
else
{
// This extra string is not in the dictionary, it has to be added
score -= 2;
targetSize++;
}
}
// We crossed the maximum number of strings (leave one free for the work buffer),
// do not consider this candidate
if (targetSize > 254) return -1;
}
return score;
}
void DictEncoder::cleanupDictionary()
{
// Get rid of dictionary entries which are not needed anymore
bool isClean = false;
while (!isClean)
{
isClean = true;
for (auto iter = dictionary.begin(); iter < dictionary.end(); iter++)
{
if (!iter->empty()) continue;
// Found an empty string in the dictionary - remove it and adapt encoding
isClean = false;
uint8_t obsoleteStrIdx = iter - dictionary.begin();
dictionary.erase(iter);
for (auto &encoding : encodings) for (auto &byte : *encoding)
{
if (byte >= obsoleteStrIdx) byte--;
}
break;
}
}
}
bool DictEncoder::optimizeSplit()
{
// XXX - The code here is not the best one - redesign and algorithm improvements
// (like - consider optimizing as a game and apply alpha-beta scheme to select the best
// candidate, incorporate frequency encoding into candidate evaluation) could result in
// a slightly better compression. But currently we have very few strings, and the dictionary
// compression is disabled by default - so right now this is not worth the effort.
if (dictionary.size() >= 255) return false; // up to 255 entries in the dictionary are possible
// First select words, which can potentially be extracted to new substrings
std::vector<std::string> candidateList;
extractWords(candidateList);
// XXX possible future improvement: find a couple of largest common substrings and add them as candidates too
// Now find the best word to be extracted
auto bestCandidate = candidateList.end();
int32_t bestCandidateScore = 0;
for (auto iter = candidateList.begin(); iter < candidateList.end(); iter++)
{
int32_t candidateScore = evaluateCandidate(*iter);
if (candidateScore > bestCandidateScore)
{
bestCandidate = iter;
bestCandidateScore = candidateScore;
}
}
// Only allow for replacements that brings some size benefit
if (bestCandidateScore < 1) return false;
// Extract the best candidate to a separate string
// First add our selected candidate to the dictionary and replace all strings which are equal to it
auto &selectedStr = *bestCandidate;
uint8_t selectedStrIdx = dictionary.size();
dictionary.push_back(selectedStr);
for (auto iter = dictionary.begin(); iter < dictionary.end(); iter++)
{
uint8_t currentStrIdx = iter - dictionary.begin();
if (*iter != selectedStr || selectedStrIdx == currentStrIdx) continue;
// Replace the current string with the new one
for (auto &encoding : encodings) for (auto &byte : *encoding)
{
if (byte == currentStrIdx) byte = selectedStrIdx;
}
// Mark obsolete dictionary entry as free for removal
iter->clear();
}
cleanupDictionary();
// Now handle normal cases, when the string needs to be split
// XXX - this is not time-effective, optimize this in the future
bool optimizeAgain = true;
bool nexIteration = true;
while (nexIteration)
{
nexIteration = false;
for (auto iter = dictionary.begin(); iter < dictionary.end(); iter++)
{
auto &currentStr = *iter;
uint8_t currentStrIdx = iter - dictionary.begin();
auto selectedStrIter = std::find(dictionary.begin(), dictionary.end(), selectedStr);
selectedStrIdx = selectedStrIter - dictionary.begin();
if (currentStrIdx == selectedStrIdx) continue;
// Check if the current string contains the selected string
auto pos = currentStr.find(selectedStr);
if (pos == std::string::npos) continue;
nexIteration = true;
optimizeAgain = true;
// Prepare replacement sequence for 'selectedStrIdx'
std::vector<uint8_t> replacement;
if (pos == 0)
{
// The current dictionary string starts with our selected substring
replacement.push_back(selectedStrIdx);
currentStr = currentStr.substr(selectedStr.size(), currentStr.size() - selectedStr.size());
if (!currentStr.empty())
{
// Check if the remaining string is present somewhere else in the dictionary; if so, reuse it
uint8_t pos2 = currentStrIdx;
for (auto iter2 = dictionary.begin(); iter2 < dictionary.end(); iter2++)
{
if (iter2 == iter) continue;
if (currentStr == *iter2)
{
dictionary[pos2].clear();
pos2 = iter2 - dictionary.begin();
break;
}
}
replacement.push_back(pos2);
}
}
else
{
// The current dictionary string does not start with our selected substring
std::string newStr = currentStr.substr(0, pos);
currentStr = currentStr.substr(pos, currentStr.size() - pos);
// Check if the newStr is present somewhere else in the dictionary; if so, reuse it
bool found = false;
uint8_t pos2 = 0;
for (auto iter2 = dictionary.begin(); iter2 < dictionary.end(); iter2++)
{
if (*iter2 == newStr)
{
found = true;
pos2 = iter2 - dictionary.begin();
break;
}
}
if (found)
{
replacement.push_back(pos2);
}
else
{
dictionary.push_back(newStr);
replacement.push_back(dictionary.size() - 1);
}
replacement.push_back(currentStrIdx);
}
// Now replace all the occurences of 'selectedStrIdx' with a 'replacement' vector
for (auto &encoding : encodings)
{
for (uint8_t idx = 0; idx < encoding->size(); idx++)
{
if ((*encoding)[idx] != currentStrIdx) continue;
// Perform replacement
encoding->erase(encoding->begin() + idx);
for (const auto replacementIdx : replacement)
{
encoding->insert(encoding->begin() + idx, replacementIdx);
idx++;
}
idx--;
}
}
cleanupDictionary();
}
}
return optimizeAgain;
}
bool DictEncoder::optimizeJoin()
{
// Try to optimize by joining two substrings
// XXX - possible future improvement
return false;
}
void DictEncoder::optimizeOrder()
{
// XXX - it would be better to do this after the dictionary is compressed
// Try to optimize order of strings in the dictionary for fastest display
// - put the shorter substrings first
// - for strings with roughly the same length put more frequently used first
typedef struct Entry
{
uint32_t penalty;
std::string word;
} Entry;
// Create copy of the dictionary with optimization info
std::vector<Entry> optimizedOrder;
for (auto iter = dictionary.begin(); iter < dictionary.end(); iter++)
{
const auto &dictionaryStr = *iter;
uint8_t sizePenalty = dictionaryStr.size() / 3;
uint16_t freqPenalty = 65535;
// Calculate penalty for low occurence frequency
for (auto &encoding : encodings)
{
for (auto &byte : *encoding)
{
if (byte == (iter - dictionary.begin())) freqPenalty--;
}
}
// Create new entry
Entry newEntry;
newEntry.word = dictionaryStr;
newEntry.penalty = sizePenalty * 65536 + freqPenalty;
optimizedOrder.push_back(newEntry);
}
// Determine ordering - sort from smallest penalty to largest
std::sort(optimizedOrder.begin(), optimizedOrder.end(),
[](const Entry &e1, const Entry &e2) { return e1.penalty < e2.penalty; });
// Create helper table for reordering
std::vector<uint8_t> reorderTable;
for (uint8_t idx1 = 0; idx1 < dictionary.size(); idx1++)
{
uint8_t idx2 = 0;
while (dictionary[idx1] != optimizedOrder[idx2].word) idx2++;
reorderTable.push_back(idx2);
}
// Perform the reordering
for (auto &encoding : encodings) for (auto &byte : *encoding)
{
byte = reorderTable[byte];
}
for (uint8_t idx = 0; idx < dictionary.size(); idx++) dictionary[idx] = optimizedOrder[idx].word;
}
void DictEncoder::process(StringEntryList &outDictionary)
{
if (dictionary.empty()) return;
// Optimize as long as it brings any improvement
while (optimizeSplit() || optimizeJoin()) ;
optimizeOrder();
// Export the dictionary to external format
outDictionary.type = ListType::DICTIONARY;
outDictionary.name = "DICTIONARY";
for (const auto &dictionaryStr : dictionary)
{
StringEntry newEntry = { true, true, true, true, "", dictionaryStr };
outDictionary.list.push_back(newEntry);
}
// Adapt the encoding to external format (0 = end of string)
for (auto &encoding : encodings)
{
for (auto &byte : *encoding) byte++;
encoding->push_back(0);
}
}
bool DataSet::isCompressionLvl2(const StringEntryList &list) const
{
return (GLOBAL_ConfigOptions["COMPRESSION_LVL_2"] && list.type == ListType::STRINGS_BASIC);
}
void DataSet::addStrings(const StringEntryList &stringList)
{
// Import the new list of strings
stringEntryLists.push_back(stringList);
stringEncodedLists.emplace_back();
// Clear strings not relevant for the current configuration
while (1)
{
if (isRelevant(stringEntryLists.back().list.back())) break;
stringEntryLists.back().list.pop_back();
if (stringEntryLists.back().list.empty())
{
ERROR(std::string("no valid strings in layout '") + layoutName() + "', list'" + stringEntryLists.back().name + "'");
}
}
// Clear the output content - make sure it is not valid anymore
outFileContent.clear();
}
void DataSet::process()
{
std::cout << "processing file '" << CMD_cnfFile << "', layout '" << layoutName() << "'" << std::endl;
generateConfigDepStrings();
validateLists();
encodeStringsDict();
calculateFrequencies();
encodeStringsFreq();
prepareOutput();
}
const std::string &DataSet::getOutput()
{
if (outFileContent.empty())
{
process();
}
return outFileContent;
}
void DataSet::generateConfigDepStrings()
{
// Generate string to show the build features
std::string featureStr;
std::string featureStrM65 = "\r";
// Tape support features
if (GLOBAL_ConfigOptions["TAPE_NORMAL"] && GLOBAL_ConfigOptions["TAPE_TURBO"])
{
featureStr += "TAPE LOAD NORMAL TURBO\r";
featureStrM65 += "TAPE : LOAD NORMAL TURBO\r";
}
else if (GLOBAL_ConfigOptions["TAPE_NORMAL"])
{
featureStr += "TAPE LOAD NORMAL\r";
featureStrM65 += "TAPE : LOAD NORMAL\r";
}
else if (GLOBAL_ConfigOptions["TAPE_TURBO"])
{
featureStr += "TAPE LOAD TURBO\r";
featureStrM65 += "TAPE : LOAD TURBO\r";
}
// IEC support features
if (GLOBAL_ConfigOptions["IEC"])
{
featureStr += "IEC";
featureStrM65 += "IEC :";
bool extendedIEC = false;
if (GLOBAL_ConfigOptions["IEC_BURST_CIA1"])
{
featureStr += " BURST1";
extendedIEC = true;
}
if (GLOBAL_ConfigOptions["IEC_BURST_CIA2"])
{
featureStr += " BURST2";
extendedIEC = true;
}
if (GLOBAL_ConfigOptions["IEC_BURST_MEGA65"])
{
featureStr += " BURST";
featureStrM65 += " BURST";
extendedIEC = true;
}
if (GLOBAL_ConfigOptions["IEC_DOLPHINDOS"])
{
featureStr += " DOLPHIN";
featureStrM65 += " DOLPHIN";
extendedIEC = true;
}
if (GLOBAL_ConfigOptions["IEC_JIFFYDOS"])
{
featureStr += " JIFFY";
featureStrM65 += " JIFFY";
extendedIEC = true;
}
if (!extendedIEC)
{
featureStr += " NORMAL ONLY";
featureStrM65 += " NORMAL ONLY";
}
featureStr += "\r";
featureStrM65 += "\r";
}
else
{
featureStrM65 += "IEC : NO\r";
}
// RS-232 support features
if (GLOBAL_ConfigOptions["RS232_ACIA"]) featureStr += "ACIA 6551\r";
if (GLOBAL_ConfigOptions["RS232_UP2400"]) featureStr += "UP2400\r";
if (GLOBAL_ConfigOptions["RS232_UP9600"]) featureStr += "UP9600\r";
featureStrM65 += "RS-232 : NO\r";
// CBDOS features
featureStrM65 += "CBDOS : NO FDD/SD/RAM SUPPORT\r";
// Keyboard support features
if (GLOBAL_ConfigOptions["KEYBOARD_C128"]) featureStr += "KBD 128\r";
// Add strings to appropriate list
for (auto &stringEntryList : stringEntryLists)
{
if (stringEntryList.name != std::string("misc")) continue;
// List found
if (GLOBAL_ConfigOptions["SHOW_FEATURES"] || GLOBAL_ConfigOptions["MB_M65"])
{
StringEntry newEntry1 = { true, true, true, true, true, "STR_PAL", "PAL\r" };
StringEntry newEntry2 = { true, true, true, true, true, "STR_NTSC", "NTSC\r" };
stringEntryList.list.push_back(newEntry1);
stringEntryList.list.push_back(newEntry2);
}
if (GLOBAL_ConfigOptions["SHOW_FEATURES"])
{
StringEntry newEntry = { true, true, true, true, true, "STR_FEATURES", featureStr };
stringEntryList.list.push_back(newEntry);
}
if (GLOBAL_ConfigOptions["MB_M65"])
{
StringEntry newEntry = { false, true, true, false, false, "STR_SI_FEATURES", featureStrM65 };
stringEntryList.list.push_back(newEntry);
}
if (!GLOBAL_ConfigOptions["BRAND_CUSTOM_BUILD"] || GLOBAL_ConfigOptions["MB_M65"])
{
StringEntry newEntry = { true, true, true, true, true, "STR_PRE_REV", "RELEASE " };
stringEntryList.list.push_back(newEntry);
}
break;
}
}
void DataSet::validateLists()
{
for (const auto &stringEntryList : stringEntryLists)
{
for (const auto &stringEntry : stringEntryList.list)
{
// Check for maximum allowed string length
if (stringEntry.string.length() > 255) ERROR("string cannot be longer than 255 characters");
// Check mor maximum keyword length
if (stringEntryList.type == ListType::KEYWORDS)
{
if (stringEntry.string.length() > 16) ERROR("keyword cannot be longer than 16 characters");
tk__max_keyword_len = std::max(tk__max_keyword_len, (uint8_t) stringEntry.string.length());
}
// Update maximum alias length too
maxAliasLen = std::max(maxAliasLen, stringEntry.alias.length());
for (const auto &character : stringEntry.string)
{
if ((unsigned char) character >= 0x80)
{
ERROR(std::string("character above 0x80 in string '") + stringEntry.string + "'");
}
}
}
}
}
void DataSet::encodeStringsDict()
{
DictEncoder dictEncoder;
// Add strings for dictionary compresssion
for (uint8_t idx = 0; idx < stringEntryLists.size(); idx++)
{
const auto &stringEntryList = stringEntryLists[idx];
auto &stringEncodedList = stringEncodedLists[idx];
// Skip lists not to be encoded using the dictionary
if (!isCompressionLvl2(stringEntryList)) continue;
stringEncodedList.resize(stringEntryList.list.size());
for (uint8_t idxEntry = 0; idxEntry < stringEntryList.list.size(); idxEntry++)
{
dictEncoder.addString(stringEntryList.list[idxEntry].string,
&stringEncodedList[idxEntry]);
}
}
// Perform the compression
StringEntryList dictionary;
dictEncoder.process(dictionary);
// Add new lists
stringEntryLists.push_back(dictionary);
stringEncodedLists.emplace_back();
}
void DataSet::calculateFrequencies()
{
as1n.clear();
as3n.clear();
std::map<char, uint16_t> freqMapGeneral; // general character frequency map
std::map<char, uint16_t> freqMapKeywords; // frequency map for keywords
// Calculate frequencies of characters in the strings
for (const auto &stringEntryList : stringEntryLists)
{
// Skip lists encoded by the dictionary
if (isCompressionLvl2(stringEntryList)) continue;
for (const auto &stringEntry : stringEntryList.list)
{
if (!isRelevant(stringEntry)) continue;
for (const auto &character : stringEntry.string)
{
freqMapGeneral[character]++;
if (stringEntryList.type == ListType::KEYWORDS) freqMapKeywords[character]++;
}
}
}
// Sort characters by frequency
std::vector<char> freqVector1;
for (auto iter = freqMapGeneral.begin(); iter != freqMapGeneral.end(); ++iter)
{
freqVector1.push_back(iter->first);
}
std::sort(freqVector1.begin(), freqVector1.end(), [&freqMapGeneral](char e1, char e2)
{ return freqMapGeneral[e2] < freqMapGeneral[e1]; });
// Check if minimal amount of characters needed, below 15 is not supported by the 6502 side code
if (freqVector1.size() < 15) ERROR(std::string("not enough distinct characters in layout '") + layoutName() + "', at least 15 needed");
// Extract 14 most frequent characters to be encoded as 1 nibble
for (uint8_t idx = 0; idx < 14; idx++)
{
as1n.push_back(freqVector1[idx]);
}
// Now sort them by frequency in keywords, in descending order - this will speed up the tokenizer a little
std::sort(as1n.begin(), as1n.end(), [&freqMapKeywords](char e1, char e2) { return freqMapKeywords[e2] > freqMapKeywords[e1]; });
// Extract characters to be encoded as 3 nibbles, which actually exist in keywords
std::vector<char> freqVector2;
tk__packed_as_3n = 0;
for (uint8_t idx = 14; idx < freqVector1.size(); idx++)
{
const auto &character = freqVector1[idx];
if (freqMapKeywords[character] > 0)
{
as3n.push_back(character);
tk__packed_as_3n++;
}
else
{
freqVector2.push_back(character);
}
}
tk__packed_as_3n = std::max(tk__packed_as_3n, (uint8_t) 1);
// Again, sort them by frequency in keywords, in descending order - this will speed up the tokenizer a little
std::sort(as3n.begin(), as3n.end(), [&freqMapKeywords](char e1, char e2) { return freqMapKeywords[e2] > freqMapKeywords[e1]; });
// Finally extract the remaining characters to be encoded as 3 nibbles
for (const auto &character : freqVector2)
{
as3n.push_back(character);
}
}
void DataSet::encodeByFreq(const std::string &plain, StringEncoded &encoded) const
{
bool fullByte = true;
auto push1n = [&](uint8_t val) // push 1 nibble - encoded character or 0xF mark
{
if (fullByte)
{
encoded.push_back(val);
fullByte = false;
}
else
{
encoded.back() += val * 0x10;
fullByte = true;
}
};
auto push2n = [&](uint8_t val) // push the remaining nibbles for 3-niobble encoded characters
{
if (fullByte)
{
encoded.push_back(val);
}
else
{
// Encode byte in a way to be easily decoded by 6502
encoded.back() += (val / 0x10) * 0x10;
encoded.push_back(val % 0x10);
}
};
// Encode every single character by frequency, put them in the output vector
for (const char &character : plain)
{
auto iterEncoding1 = std::find(as1n.begin(), as1n.end(), character);
if (iterEncoding1 != as1n.end())
{
push1n(std::distance(as1n.begin(), iterEncoding1) + 1);
}
else
{
push1n(0x0F);
auto iterEncoding3 = std::find(as3n.begin(), as3n.end(), character);
if (iterEncoding3 == as3n.end())
{
ERROR("internal error in 'encodeByFreq'");
}
push2n(std::distance(as3n.begin(), iterEncoding3) + 1);
}
}
// Make sure the last byte of encoded stream is 0
if (encoded.size() == 0 || encoded.back() != 0) encoded.push_back(0);
}
void DataSet::encodeStringsFreq()
{
// Encode every relevant string from every list - by character frequency
for (uint8_t idx = 0; idx < stringEntryLists.size(); idx++)
{
const auto &stringEntryList = stringEntryLists[idx];
auto &stringEncodedList = stringEncodedLists[idx];
// Skip lists encoded by the dictionary
if (isCompressionLvl2(stringEntryList)) continue;
// Perform frequency encoding of the list
for (const auto &stringEntry : stringEntryList.list)
{
stringEncodedList.emplace_back();
auto &stringEncoded = stringEncodedList.back();
if (isRelevant(stringEntry))
{
encodeByFreq(stringEntry.string, stringEncoded);
}
}
}
}
void DataSet::putCharEncoding(std::ostringstream &stream, uint8_t idx, char character, bool is3n)
{
stream << "\t!byte $" << std::uppercase << std::hex <<
std::setfill('0') << std::setw(2) << +character <<
" ; " << std::setfill(is3n ? '0' : ' ') << std::setw(2) << +idx;
std::string petscii;
if (character == 0x20)
{
petscii = " = SPACE";
}
else if (character == 0x27)
{
petscii = " = APOSTROPHE";
}
else if (character == 0x0D)
{
petscii = " = RETURN";
}
else if (character > ' ' && character < 'a')
{
petscii = std::string(" = '") + character + "'";
}
stream << petscii << std::endl;
}
void DataSet::prepareOutput_1n_3n(std::ostringstream &stream)
{
uint8_t idx;
// Export all nibble-encoded characters
stream << std::endl << "!macro PUT_PACKED_AS_1N { ; characters encoded as 1 nibble" << std::endl << std::endl;
idx = 0;
for (const auto& encoding : as1n)
{
putCharEncoding(stream, ++idx, encoding, false);
}
stream << "}" << std::endl;
// Export all byte-encoded characters
stream << std::endl << "!macro PUT_PACKED_AS_3N { ; characters encoded as 3 nibbles" << std::endl << std::endl;
idx = 0;
for (const auto& encoding : as3n)
{
if (idx == tk__packed_as_3n) stream << std::endl <<
"\t; Characters below are not used by any BASIC keyword" << std::endl << std::endl;
putCharEncoding(stream, ++idx, encoding, true);
}
stream << "}" << std::endl;
}
void DataSet::prepareOutput_labels(std::ostringstream &stream,
const StringEntryList &stringEntryList,
const StringEncodedList &stringEncodedList)
{
// For the dictionary we do not have any labels
if (stringEntryList.type == ListType::DICTIONARY) return;
stream << std::endl;
for (uint8_t idx = 0; idx < stringEncodedList.size(); idx++)
{
const auto &stringEntry = stringEntryList.list[idx];
const auto &stringEncoded = stringEncodedList[idx];
if (!stringEncoded.empty())
{
stream << "!set IDX__" << stringEntry.alias <<
std::string(maxAliasLen - stringEntry.alias.length(), ' ') << " = $" <<
std::uppercase << std::hex << std::setfill('0') << std::setw(2) << +idx << std::endl;
}
}
}
void DataSet::prepareOutput_packed(std::ostringstream &stream,
const StringEntryList &stringEntryList,
const StringEncodedList &stringEncodedList)
{
if (isCompressionLvl2(stringEntryList))
{
stream << std::endl << "!macro PUT_PACKED_DICT_";
}
else
{
stream << std::endl << "!macro PUT_PACKED_FREQ_";
}
stream << stringEntryList.name << " {" << std::endl << std::endl;
enum LastStr { NONE, SKIPPED, WRITTEN } lastStr = LastStr::NONE;
for (uint8_t idxString = 0; idxString < stringEncodedList.size(); idxString++)
{
const auto &stringEncoded = stringEncodedList[idxString];
if (stringEncoded.empty())
{
if (stringEntryList.type == ListType::DICTIONARY) ERROR("internal error"); // should never happen
if (lastStr == LastStr::WRITTEN) stream << std::endl;
stream << "\t!byte $00 ; skipped " << stringEntryList.list[idxString].alias << std::endl;
lastStr = LastStr::SKIPPED;
}
else
{
if (lastStr != LastStr::NONE) stream << std::endl;
// Output the label - as a comment
if (stringEntryList.type != ListType::DICTIONARY)
{
stream << "\t; IDX__" << stringEntryList.list[idxString].alias << std::endl;
}
// Output the source string - as a comment
stream << "\t; '";
for (auto &character : stringEntryList.list[idxString].string)
{
if (character >= 32 && character <= 132 && character != 39 && character != 34)
{
stream << character;
}
else if (character == 13)
{
stream << "<return>";
}
else
{
stream << "_";
}
}
stream << "'" << std::endl;
// Output the encoding
stream << "\t!byte ";
bool first = true;
for (const auto &charEncoded : stringEncoded)
{
if (first)
{
first = false;
}
else
{
stream << ", ";
}
stream << "$" << std::uppercase << std::hex << std::setfill('0') << std::setw(2) << +charEncoded;
}
stream << std::endl;
lastStr = LastStr::WRITTEN;
}
}
// For the token list - put the 'end of keywords' mark
if (stringEntryList.type == ListType::KEYWORDS)
{
stream << std::endl << "\t; Marker - end of the keyword list" << std::endl;
stream << "\t!byte $FF, $FF" << std::endl;
}
stream << "}" << std::endl;
}
void DataSet::prepareOutput()
{
// Convert our encoded strings to a KickAssembler source
std::ostringstream stream;
// Export 1-nibble and 3-nibble encoding data
prepareOutput_1n_3n(stream);
// Export additional data for the tokenizer
stream << std::endl << "!set TK__PACKED_AS_3N = $" << std::hex << +tk__packed_as_3n <<
std::endl << "!set TK__MAX_KEYWORD_LEN = " << std::dec << +tk__max_keyword_len << std::endl;
// Export encoded strings
for (uint8_t idx = 0; idx < stringEntryLists.size(); idx++)
{
const auto &stringEntryList = stringEntryLists[idx];
const auto &stringEncodedList = stringEncodedLists[idx];
if (stringEncodedList.empty()) continue;
// Export labels for the current list
prepareOutput_labels(stream, stringEntryList, stringEncodedList);
// For the token list - put the number of tokens available
if (stringEntryList.type == ListType::KEYWORDS)
{
stream << std::endl << "!set TK__MAXTOKEN_" << stringEntryList.name << " = " <<
std::dec << stringEncodedList.size() << std::endl;
}
// Export the packed data
prepareOutput_packed(stream, stringEntryList, stringEncodedList);
}
// Finalize the file stream
stream << std::endl;
outFileContent = stream.str();
}
//
// Common helper functions
//
void parseConfigFile()
{
GLOBAL_ConfigOptions.clear();
// Open the configuration file
std::ifstream cnfFile;
cnfFile.open(CMD_cnfFile);
if (!cnfFile.good()) ERROR("unable to open config file");
// Parse the file
size_t lineNum = 0;
while (!cnfFile.eof())
{
lineNum++;
std::string workStr;
// Read a single line, remove leading spaces and tabs
std::getline(cnfFile, workStr);
if (cnfFile.bad()) ERROR("error reading configuration file");
workStr = std::regex_replace(workStr, std::regex("^[ \t]+"), "");
// Split the line into tokens
std::vector<std::string> tokens;
std::istringstream workStream(workStr);
std::string token;
while(std::getline(workStream, token, ' '))
{
if (token.empty()) continue;
tokens.push_back(token);
}
if (tokens.empty()) continue;
// Only accept lines which are boolean config options set to YES
if (tokens.size() < 4 ||
tokens[0].compare(";;") != 0 ||
tokens[1].compare("#CONFIG#") != 0 ||
tokens[3].compare("YES") != 0)
{
continue;
}
// Add definitin to config option map
if (tokens[2].empty()) ERROR(std::string("error parsing config file - line ") + std::to_string(lineNum));
GLOBAL_ConfigOptions[tokens[2]] = true;
}
cnfFile.close();
}
void printUsage()
{
std::cout << "\n" <<
"usage: generate_strings [-o <out file>] [-c <configuration file>]" << "\n\n";
}
void printBanner()
{
printBannerLineTop();
std::cout << "// Generating compressed messages and BASIC tokens\n";
printBannerLineBottom();
}
void parseCommandLine(int argc, char **argv)
{
int opt;
// Retrieve command line options
while ((opt = getopt(argc, argv, "o:c:")) != -1)
{
switch(opt)
{
case 'o': CMD_outFile = optarg; break;
case 'c': CMD_cnfFile = optarg; break;
default: printUsage(); ERROR();
}
}
}
void writeStrings()
{
std::string outputString;
if (GLOBAL_ConfigOptions["PLATFORM_COMMANDER_X16"])
{
DataSetX16 dataSetX16;
// Add input data to computation objects
dataSetX16.addStrings(GLOBAL_Keywords_V2);
dataSetX16.addStrings(GLOBAL_Keywords_01);
dataSetX16.addStrings(GLOBAL_Errors);
dataSetX16.addStrings(GLOBAL_MiscStrings);
// Retrieve the results
outputString = dataSetX16.getOutput();
}
else if (GLOBAL_ConfigOptions["PLATFORM_COMMODORE_64"] && GLOBAL_ConfigOptions["MB_M65"])
{
DataSetM65 dataSetM65;
// Add input data to computation objects
dataSetM65.addStrings(GLOBAL_Keywords_V2);
dataSetM65.addStrings(GLOBAL_Keywords_01);
dataSetM65.addStrings(GLOBAL_Keywords_04);
dataSetM65.addStrings(GLOBAL_Keywords_06);
dataSetM65.addStrings(GLOBAL_Errors);
dataSetM65.addStrings(GLOBAL_MiscStrings);
// Retrieve the results
outputString = dataSetM65.getOutput();
}
else if (GLOBAL_ConfigOptions["PLATFORM_COMMODORE_64"] && GLOBAL_ConfigOptions["ROM_CRT"])
{
DataSetCRT dataSetCRT;
// Add input data to computation objects
dataSetCRT.addStrings(GLOBAL_Keywords_V2);
dataSetCRT.addStrings(GLOBAL_Keywords_01);
dataSetCRT.addStrings(GLOBAL_Errors);
dataSetCRT.addStrings(GLOBAL_MiscStrings);
// Retrieve the results
outputString = dataSetCRT.getOutput();
}
else if (GLOBAL_ConfigOptions["PLATFORM_COMMODORE_64"] && GLOBAL_ConfigOptions["MB_U64"])
{
DataSetU64 dataSetU64;
// Add input data to computation objects
dataSetU64.addStrings(GLOBAL_Keywords_V2);
dataSetU64.addStrings(GLOBAL_Keywords_01);
dataSetU64.addStrings(GLOBAL_Errors);
dataSetU64.addStrings(GLOBAL_MiscStrings);
// Retrieve the results
outputString = dataSetU64.getOutput();
}
else if (GLOBAL_ConfigOptions["PLATFORM_COMMODORE_64"])
{
DataSetSTD dataSetSTD;
// Add input data to computation objects
dataSetSTD.addStrings(GLOBAL_Keywords_V2);
dataSetSTD.addStrings(GLOBAL_Keywords_01);
dataSetSTD.addStrings(GLOBAL_Errors);
dataSetSTD.addStrings(GLOBAL_MiscStrings);
// Retrieve the results
outputString = dataSetSTD.getOutput();
}
else
{
ERROR("unable to determine string set");
}
// Remove old file
unlink(CMD_outFile.c_str());
// Open output file for writing
std::ofstream outFile(CMD_outFile, std::fstream::out | std::fstream::trunc);
if (!outFile.good()) ERROR(std::string("can't open oputput file '") + CMD_outFile + "'");
// Write header
outFile << ";\n; Generated file - do not edit\n;";
// Write packed strings
outFile << std::endl << std::endl;
outFile << outputString;
outFile << std::endl << std::endl;
// Close the file
outFile.close();
std::cout << std::string("compressed strings written to: '") + CMD_outFile + "'\n\n";
}
//
// Main function
//
int main(int argc, char **argv)
{
printBanner();
parseCommandLine(argc, argv);
parseConfigFile();
writeStrings();
return 0;
}