From a3a9dfac0a141c66266b355ec1a00dc8092d2989 Mon Sep 17 00:00:00 2001 From: bbbradsmith Date: Sun, 21 Apr 2024 03:44:44 -0400 Subject: [PATCH] Generate settings and localizable text data Sketching the public interfaces to core and gui --- .github/workflows/build.yml | 8 + cmd/cmd.cpp | 2 +- core/core.cpp | 6 +- core/core.vcxproj | 1 + core/core.vcxproj.filters | 3 + core/enums_data.h | 129 +++++ enums/english.txt | 26 + enums/japanese.txt | 4 + enums/makefile | 11 + enums/nsfplayenums.py | 973 ++++++++++++++++++++++++++++++++++++ enums/settings.txt | 31 ++ gui/gui.cpp | 5 - gui/gui.vcxproj | 2 + gui/gui.vcxproj.filters | 6 + include/nsfplaycore.h | 230 ++++++++- include/nsfplayenums.h | 70 +++ include/nsfplaygui.h | 26 +- makefile | 8 +- readme.md | 3 + 19 files changed, 1522 insertions(+), 22 deletions(-) create mode 100644 core/enums_data.h create mode 100644 enums/english.txt create mode 100644 enums/japanese.txt create mode 100644 enums/makefile create mode 100644 enums/nsfplayenums.py create mode 100644 enums/settings.txt create mode 100644 include/nsfplayenums.h diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 45a80a3..ecfc081 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,6 +10,14 @@ concurrency: cancel-in-progress: true jobs: + check-gen: + name: Check Generated Files + runs-on: ubuntu-latest + steps: + - name: Checkout Files + uses: actions/checkout@v4 + - name: Verify Enums match Generated Files + run: make enums_verify build-msvc: name: Windows MSVC diff --git a/cmd/cmd.cpp b/cmd/cmd.cpp index d1170b7..3389e11 100644 --- a/cmd/cmd.cpp +++ b/cmd/cmd.cpp @@ -6,5 +6,5 @@ int main() { printf("nsfplac stub\n"); - return nsfplaycore::test(); + return 0; } diff --git a/core/core.cpp b/core/core.cpp index 342f13f..371899f 100644 --- a/core/core.cpp +++ b/core/core.cpp @@ -1,8 +1,4 @@ // stub #include - -int nsfplaycore::test() -{ - return 3; -} +#include "enums_data.h" diff --git a/core/core.vcxproj b/core/core.vcxproj index e29db39..207eb87 100644 --- a/core/core.vcxproj +++ b/core/core.vcxproj @@ -179,6 +179,7 @@ + diff --git a/core/core.vcxproj.filters b/core/core.vcxproj.filters index 012c33f..88a658d 100644 --- a/core/core.vcxproj.filters +++ b/core/core.vcxproj.filters @@ -19,5 +19,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/core/enums_data.h b/core/enums_data.h new file mode 100644 index 0000000..81065ae --- /dev/null +++ b/core/enums_data.h @@ -0,0 +1,129 @@ +#pragma once +// generated by nsfplayenums.py +// Sun Apr 21 03:40:40 2024 + +#include "../include/nsfplayenums.h" + +const int32_t NSFPD_LIST[NSFP_LIST_COUNT] = { + 0, 2, +}; + +typedef struct { + const char* key; + int32_t text; +} NSFSetGroupData; +const NSFSetGroupData NSFPD_GROUP[NSFP_GROUP_COUNT] = { + { "MAIN", 4 }, + { "APU0", 8 }, + { "APU1", 12 }, +}; + +typedef struct { + const char* key; + int32_t unit, text; +} NSFChannelData; +const NSFChannelData NSFPD_CHANNEL[NSFP_CHANNEL_COUNT] = { + { "SQU0", 0, 16 }, + { "SQU1", 0, 18 }, + { "TRI", 1, 20 }, + { "NSE", 1, 22 }, + { "DPCM", 1, 24 }, +}; + +typedef struct { + const char* key; + int32_t group, text; + int32_t default_int, min_int, max_int, list; + const char* default_str; +} NSFSetData; +const NSFSetData NSFPD_SET[NSFP_SET_COUNT] = { + { "VOLUME", 0, 26, 500, 0, 1000, -1,NULL }, + { "SAMPLERATE", 0, 30, 48000, 1000,4000000, -1,NULL }, + { "STEREO", 0, 34, 1, 0, 1, 0,NULL }, + { "TITLE_FORMAT", 0, 38, 0, 0, 0, -1,"%L (%n/%e) %T - %A" }, + { "LOCALE", 0, 42, 0, 0, 1, 1,NULL }, + { "SQU0_ON", 1, 46, 1, 0, 1, 0,NULL }, + { "SQU0_VOL", 1, 50, 500, 0, 1000, -1,NULL }, + { "SQU0_PAN", 1, 54, 500, 0, 1000, -1,NULL }, + { "SQU1_ON", 1, 58, 1, 0, 1, 0,NULL }, + { "SQU1_VOL", 1, 62, 500, 0, 1000, -1,NULL }, + { "SQU1_PAN", 1, 66, 500, 0, 1000, -1,NULL }, + { "TRI_ON", 2, 70, 1, 0, 1, 0,NULL }, + { "TRI_VOL", 2, 74, 500, 0, 1000, -1,NULL }, + { "TRI_PAN", 2, 78, 500, 0, 1000, -1,NULL }, + { "NSE_ON", 2, 82, 1, 0, 1, 0,NULL }, + { "NSE_VOL", 2, 86, 500, 0, 1000, -1,NULL }, + { "NSE_PAN", 2, 90, 500, 0, 1000, -1,NULL }, + { "DPCM_ON", 2, 94, 1, 0, 1, 0,NULL }, + { "DPCM_VOL", 2, 98, 500, 0, 1000, -1,NULL }, + { "DPCM_PAN", 2, 102, 500, 0, 1000, -1,NULL }, +}; + +typedef struct { + const char* key; + int32_t type, text; +} NSFPropData; +const NSFPropData NSFPD_PROP[NSFP_PROP_COUNT] = { + { "SONGCOUNT",1, 106 }, + { "LONG",2, 108 }, + { "TITLE",3, 110 }, + { "INFO",4, 112 }, + { "BLOB",5, 114 }, +}; + +typedef struct { + const char* key; + int32_t type, text; +} NSFSongPropData; +const NSFSongPropData NSFPD_SONGPROP[NSFP_SONGPROP_COUNT] = { + { "INT",1, 116 }, + { "SONGTEST",1, 118 }, + { "LONG",2, 120 }, + { "TITLE",3, 122 }, + { "INFO",4, 124 }, + { "BLOB",5, 126 }, +}; + +const int32_t NSFPD_TEXT[NSFP_LIST_COUNT] = { + 128, 130, +}; + +const int32_t NSFPD_LOCAL_TEXT[NSFP_LOCALE_COUNT][132] = { +{ + 0x000000,0x000000,0x000008,0x000008,0x00001B,0x000020,0x00001B,0x000020,0x00002E,0x000033,0x00002E,0x000033,0x00002E,0x00002E,0x00002E,0x00002E, + 0x000057,0x000057,0x00005C,0x00005C,0x000061,0x000061,0x000065,0x000065,0x000069,0x000069,0x00006E,0x000075,0x00006E,0x000075,0x00007C,0x000087, + 0x00007C,0x000087,0x000092,0x000099,0x000092,0x000099,0x0000A0,0x0000AD,0x0000A0,0x0000AD,0x0000C3,0x0000C3,0x0000C3,0x0000C3,0x0000CA,0x0000CA, + 0x0000CA,0x0000CA,0x0000CA,0x0000CA,0x0000CA,0x0000CA,0x0000CA,0x0000CA,0x0000CA,0x0000CA,0x0000D1,0x0000D1,0x0000D1,0x0000D1,0x0000D1,0x0000D1, + 0x0000D1,0x0000D1,0x0000D1,0x0000D1,0x0000D1,0x0000D1,0x0000D8,0x0000D8,0x0000D8,0x0000D8,0x0000D8,0x0000D8,0x0000D8,0x0000D8,0x0000D8,0x0000D8, + 0x0000D8,0x0000D8,0x0000DE,0x0000DE,0x0000DE,0x0000DE,0x0000DE,0x0000DE,0x0000DE,0x0000DE,0x0000DE,0x0000DE,0x0000DE,0x0000DE,0x0000E4,0x0000E4, + 0x0000E4,0x0000E4,0x0000E4,0x0000E4,0x0000E4,0x0000E4,0x0000E4,0x0000E4,0x0000E4,0x0000E4,0x0000EB,0x0000EB,0x0000F6,0x0000F6,0x0000FB,0x0000FB, + 0x000101,0x000101,0x000106,0x000106,0x00010B,0x00010B,0x00010F,0x00010F,0x0000F6,0x0000F6,0x000119,0x000119,0x000101,0x000101,0x000106,0x000106, + 0x000124,0x000124,0x000129,0x000129, +}, +{ + 0x000000,0x000000,0x000008,0x000008,0x00001B,0x000020,0x00001B,0x000020,0x00002E,0x000033,0x00002E,0x000033,0x00002E,0x00002E,0x00002E,0x00002E, + 0x000057,0x000057,0x00005C,0x00005C,0x000061,0x000061,0x000065,0x000065,0x000069,0x000069,0x00006E,0x000075,0x00006E,0x000075,0x00007C,0x000087, + 0x00007C,0x000087,0x000092,0x000099,0x000092,0x000099,0x0000A0,0x0000AD,0x0000A0,0x0000AD,0x0000C3,0x0000C3,0x0000C3,0x0000C3,0x0000CA,0x0000CA, + 0x0000CA,0x0000CA,0x0000CA,0x0000CA,0x0000CA,0x0000CA,0x0000CA,0x0000CA,0x0000CA,0x0000CA,0x0000D1,0x0000D1,0x0000D1,0x0000D1,0x0000D1,0x0000D1, + 0x0000D1,0x0000D1,0x0000D1,0x0000D1,0x0000D1,0x0000D1,0x0000D8,0x0000D8,0x0000D8,0x0000D8,0x0000D8,0x0000D8,0x0000D8,0x0000D8,0x0000D8,0x0000D8, + 0x0000D8,0x0000D8,0x0000DE,0x0000DE,0x0000DE,0x0000DE,0x0000DE,0x0000DE,0x0000DE,0x0000DE,0x0000DE,0x0000DE,0x0000DE,0x0000DE,0x0000E4,0x0000E4, + 0x0000E4,0x0000E4,0x0000E4,0x0000E4,0x0000E4,0x0000E4,0x0000E4,0x0000E4,0x0000E4,0x0000E4,0x0000EB,0x0000EB,0x0000F6,0x0000F6,0x0000FB,0x0000FB, + 0x000101,0x000101,0x000106,0x000106,0x00010B,0x00010B,0x00010F,0x00010F,0x0000F6,0x0000F6,0x000119,0x000119,0x000101,0x000101,0x000106,0x000106, + 0x000124,0x000124,0x000129,0x000129, +}, +}; + +const uint8_t NSFPD_LOCAL_TEXT_DATA[0x000135] = { + 0x4F,0x66,0x66,0x00,0x4F,0x6E,0x00,0x00,0x45,0x6E,0x67,0x6C,0x69,0x73,0x68,0x00,0xE6,0x97,0xA5,0xE6,0x9C,0xAC,0xE8,0xAA,0x9E,0x00,0x00,0x4D,0x61,0x69,0x6E,0x00, + 0x4D,0x61,0x69,0x6E,0x20,0x53,0x65,0x74,0x74,0x69,0x6E,0x67,0x73,0x00,0x41,0x50,0x55,0x31,0x00,0x42,0x75,0x69,0x6C,0x74,0x2D,0x69,0x6E,0x20,0x74,0x72,0x69,0x61, + 0x6E,0x67,0x6C,0x65,0x2C,0x20,0x6E,0x6F,0x69,0x73,0x65,0x2C,0x20,0x61,0x6E,0x64,0x20,0x44,0x50,0x43,0x4D,0x2E,0x00,0x53,0x51,0x55,0x30,0x00,0x53,0x51,0x55,0x31, + 0x00,0x54,0x52,0x49,0x00,0x4E,0x53,0x45,0x00,0x44,0x50,0x43,0x4D,0x00,0x56,0x4F,0x4C,0x55,0x4D,0x45,0x00,0x56,0x6F,0x6C,0x75,0x6D,0x65,0x00,0x53,0x41,0x4D,0x50, + 0x4C,0x45,0x52,0x41,0x54,0x45,0x00,0x53,0x61,0x6D,0x70,0x6C,0x65,0x72,0x61,0x74,0x65,0x00,0x53,0x54,0x45,0x52,0x45,0x4F,0x00,0x53,0x74,0x65,0x72,0x65,0x6F,0x00, + 0x54,0x49,0x54,0x4C,0x45,0x5F,0x46,0x4F,0x52,0x4D,0x41,0x54,0x00,0x4E,0x53,0x46,0x20,0x73,0x6F,0x6E,0x67,0x20,0x74,0x69,0x74,0x6C,0x65,0x20,0x66,0x6F,0x72,0x6D, + 0x61,0x74,0x00,0x4C,0x4F,0x43,0x41,0x4C,0x45,0x00,0x53,0x51,0x55,0x30,0x4F,0x4E,0x00,0x53,0x51,0x55,0x31,0x4F,0x4E,0x00,0x54,0x52,0x49,0x4F,0x4E,0x00,0x4E,0x53, + 0x45,0x4F,0x4E,0x00,0x44,0x50,0x43,0x4D,0x4F,0x4E,0x00,0x53,0x6F,0x6E,0x67,0x20,0x63,0x6F,0x75,0x6E,0x74,0x00,0x4C,0x4F,0x4E,0x47,0x00,0x54,0x49,0x54,0x4C,0x45, + 0x00,0x49,0x4E,0x46,0x4F,0x00,0x42,0x4C,0x4F,0x42,0x00,0x49,0x4E,0x54,0x00,0x53,0x6F,0x6E,0x67,0x20,0x74,0x65,0x73,0x74,0x00,0x53,0x6F,0x6E,0x67,0x20,0x74,0x69, + 0x74,0x6C,0x65,0x00,0x54,0x45,0x58,0x54,0x00,0x45,0x52,0x52,0x4F,0x52,0x5F,0x45,0x52,0x52,0x4F,0x52,0x00, +}; + +// end of file diff --git a/enums/english.txt b/enums/english.txt new file mode 100644 index 0000000..9d348a2 --- /dev/null +++ b/enums/english.txt @@ -0,0 +1,26 @@ +LOCAL ENGLISH "English" +LOCALDEFAULT + +LOCALLIST ENABLE OFF "Off" +LOCALLIST ENABLE ON "On" + +LOCALSETGROUP MAIN "Main" "Main Settings" +LOCALSET MAIN VOLUME "Volume" "Total output loudness." +LOCALSET MAIN SAMPLERATE "Samplerate" "Audio output samplerate." +LOCALSET MAIN STEREO "Stereo" "Disable to output 1-channel mono sound." +LOCALSET MAIN TITLE_FORMAT "NSF song title format" "See documentation for details." + +LOCALPROP SONGCOUNT "Song count" +LOCALSONGPROP TITLE "Song title" +LOCALSONGPROP SONGTEST "Song test" + +LOCALUNIT APU0 "APU0" "Built-in square channels." +LOCALUNIT APU1 "APU1" "Built-in triangle, noise, and DPCM." +LOCALCHANNEL APU0 SQU0 "Square 0" +LOCALCHANNEL APU0 SQU1 "Square 1" +LOCALCHANNEL APU1 TRI "Triangle" +LOCALCHANNEL APU1 NSE "Noise" +LOCALCHANNEL APU1 DPCM "DPCM" + +LOCALTEXT TEXT "Test Text" +LOCALERROR ERROR "Test Error" diff --git a/enums/japanese.txt b/enums/japanese.txt new file mode 100644 index 0000000..d3586bf --- /dev/null +++ b/enums/japanese.txt @@ -0,0 +1,4 @@ +LOCAL JAPANESE "日本語" + +# LOCALPROP NOTFOUND "Not found" +# LOCALTEXT BAD "Not in English" diff --git a/enums/makefile b/enums/makefile new file mode 100644 index 0000000..b2208ae --- /dev/null +++ b/enums/makefile @@ -0,0 +1,11 @@ +include ../makefile.common + +.PHONY: default verify + +default: + python nsfplayenums.py + +verify: + python nsfplayenums.py -verify -nowarn + +# eventually replace -nowarn with -strict diff --git a/enums/nsfplayenums.py b/enums/nsfplayenums.py new file mode 100644 index 0000000..38b4460 --- /dev/null +++ b/enums/nsfplayenums.py @@ -0,0 +1,973 @@ +#!/usr/bin/env python3 + +# +# This generates enumeration defines used for many purposes, +# as well as tables of strings to store text needed by the core, +# and localized text variations. +# +# Input: +# TXT files in current folder, starting with settings.txt +# Generates: +# ../include/nsfplayenums.h +# ../core/enums_data.h +# + +# +# -verify +# This command line option will not replace the generated files, +# but instead will parse their contents and compare to the current TXT source, +# returning 0 if matching, or -1 if not matched. +# -strict +# Will return -1 at the end of processing if any warnings occured. +# Can be be used independently of -verify. +# -nowarn +# Disable warnings. +# -verbose +# Will print diagnostic information. +# + +# +# TXT file format +# UTF-8 +# each line creates some definition +# keys are allcaps A-Z/0-9/_ strings with no spaces +# all text values should be enclosed in quotes, and should never contain quotes +# # begins a comment, the rest of line will be ignored +# all integers are signed 32-bit +# +# A set of options that can be used for an INT setting. +# LIST list-key key-0 key-1 ... +# +# Settings definitions. +# SETGROUP creates a group that subsequent settings will be assigned to. +# The SETLIST is like a SETINT but will map its numbers to a LIST. +# SETGROUP group-key +# SETINT group-key key default min max +# SETLIST group-key key list-key default-key +# SETSTR group-key key "default" +# +# NSF Properties. +# PROPINT key +# PROPLONG key +# PROPSTR key +# PROPLINES key +# PROPBLOB key +# Song Properties. +# SONGPROPINT key +# SONGPROPLONG key +# SONGPROPSTR key +# SONGPROPLINES key +# SONGPROPBLOB key +# +# Global channel info. +# The UNIT will also generate a settings group "UNIT", with a VOL setting. +# The CHANNEL will also generate ON, VOL and PAN settings. +# The CHANNELONLIST should be used once to specify a LIST to use for the CHANNEL ON settings. +# UNIT key +# CHANNEL unit-key key +# CHANNELONLIST list-key +# +# Locales create a set of localized text for a specific language/locale. +# Allows nice text to be used instead of the internal keys. +# The LOCAL command begins a new locale, all LOCAL statements after will apply to it. +# One locale must be set as the default with LOCALDEFAULT. +# If a locale is missing strings, it will use the LOCALDEFAULT as a backup. +# If LOCALDEFAULT is missing strings, it will use the corresponding key if possible. +# The LOCALCHANNELSET command should be used once per locale to name the ON/VOL/PAN settings. +# The LOCALTEXT definition creates an enumerated string, more miscellaneous purposes if needed by the code. +# LOCALERROR is just LOCALTEXT with "ERROR_" automatically prefixed to the key. +# In place of a text value, * can be used to defer to the LOCALDEFAULT without creating a warning. +# LOCAL key "name" +# LOCALDEFAULT +# LOCALLIST list-key key "name" +# LOCALSETGROUP group-key "name" "description" +# LOCALSET key "name" "description" +# LOCALPROP key "name" +# LOCALSONGPROP key "name" +# LOCALUNIT unit-key "name" "desc" +# LOCALCHANNEL unit-key key "name" +# LOCALCHANNELSET enable-list-key "enable-name" "volume-name" "pan-name" "enable-desc" "volume-desc" "pan-desc" +# LOCALTEXT "key" "text" +# LOCALERROR "key" "text" +# +# A default LOCALES LIST and corresponding SETLIST will be automatically generated in the first defined SETGROUP. +# Each LOCAL should define a LOCALSET for it in this group. +# LOCALSET MAIN LOCALES "Language" "Display language." +# + +import sys +import os +import shlex +import argparse +import datetime + +INPUT_FIRST = "settings.txt" +INPUT_FOLDER = "." +OUTPUT_ENUM = "../include/nsfplayenums.h" +OUTPUT_DATA = "../core/enums_data.h" + +VERIFY = False +STRICT = False +NOWARN = False +VERBOSE = False + +warnings = 0 +errors = 0 + +now_string = datetime.datetime.now().strftime("%a %b %d %H:%M:%S %Y") + +# +# warnings and errors +# + +def print_error(message): + sys.stderr.write(message + "\n") + +def warn(message): + global warnings + if not NOWARN: + print_error("Warning:" + message); + warnings += 1 + +def error(message): + global errors + print_error("Error:" + message); + errors += 1 + +def terminate(): + if STRICT and (warnings > 0): error("Warning treated as error (-strict)") + if warnings > 0: print_error("%d warnings, check error log" % (warnings)) + if errors > 0: print_error("%d errors, check error log" % (errors)) + if (errors > 0): + sys.exit(-1) + print("Success.") + sys.exit(0) + +def fatal_error(message): + global errors + print_error("Fatal error: " + message); + errors += 1 + terminate() + +def verbose(message): + if VERBOSE: print(message) + +# +# parsing +# + +parse_line = 0 +parse_path = None + +defs_list = [] +defs_setgroup = [] +defs_set = [] +defs_prop = [] +defs_songprop = [] +defs_unit = [] +defs_channel = [] +defs_channelonlist = None +defs_local = [] +defs_localdefault = None + +PROP_INT = 1 +PROP_LONG = 2 +PROP_STR = 3 +PROP_LINES = 4 +PROP_BLOB = 5 + +# parsing definitions + +PARSE_KEY = 0 +PARSE_INT = 1 +PARSE_STR = 2 +PARSE_KEYS = 3 + +PARSE_DEFS = { + "LIST":[PARSE_KEY,PARSE_KEYS], + "SETGROUP":[PARSE_KEY], + "SETINT":[PARSE_KEY,PARSE_KEY,PARSE_INT,PARSE_INT,PARSE_INT], + "SETLIST":[PARSE_KEY,PARSE_KEY,PARSE_KEY,PARSE_KEY], + "SETSTR":[PARSE_KEY,PARSE_KEY,PARSE_STR], + "PROPINT":[PARSE_KEY], + "PROPLONG":[PARSE_KEY], + "PROPSTR":[PARSE_KEY], + "PROPLINES":[PARSE_KEY], + "PROPBLOB":[PARSE_KEY], + "SONGPROPINT":[PARSE_KEY], + "SONGPROPLONG":[PARSE_KEY], + "SONGPROPSTR":[PARSE_KEY], + "SONGPROPLINES":[PARSE_KEY], + "SONGPROPBLOB":[PARSE_KEY], + "UNIT":[PARSE_KEY], + "CHANNEL":[PARSE_KEY,PARSE_KEY], + "CHANNELONLIST":[PARSE_KEY], + "LOCAL":[PARSE_KEY,PARSE_STR], + "LOCALDEFAULT":[], + "LOCALLIST":[PARSE_KEY,PARSE_KEY,PARSE_STR], + "LOCALSETGROUP":[PARSE_KEY,PARSE_STR,PARSE_STR], + "LOCALSET":[PARSE_KEY,PARSE_STR,PARSE_STR], + "LOCALPROP":[PARSE_KEY,PARSE_STR], + "LOCALSONGPROP":[PARSE_KEY,PARSE_STR], + "LOCALUNIT":[PARSE_KEY,PARSE_STR,PARSE_STR], + "LOCALCHANNEL":[PARSE_KEY,PARSE_KEY,PARSE_STR], + "LOCALCHANNELSET":[PARSE_STR,PARSE_STR,PARSE_STR,PARSE_STR,PARSE_STR,PARSE_STR], + "LOCALTEXT":[PARSE_KEY,PARSE_STR], + "LOCALERROR":[PARSE_KEY,PARSE_STR], +} +PARSE_DEF_NAME = { + PARSE_KEY:"KEY", + PARSE_INT:"INT", + PARSE_STR:"STRING", + PARSE_KEYS:"KEY...", +} + +# parsing errors + +def parse_warn(message): + if (parse_path != None): + warn(parse_path + (":%d " % parse_line) + message) + else: + warn(message) + +def parse_error(message): + if (parse_path != None): + error(parse_path + (":%d " % parse_line) + message) + else: + error(message) + +def is_key(s): + if len(s) < 1: return False # can't be empty + if s[0] < 'A' or s[0] > 'Z': # start with letter + return False + for c in s: # check allowed characters + if (c >= 'A' and c <= 'Z'): continue + if (c >= '0' and c <= '9'): continue + if (c == '_'): continue + return False + return True + +# parse defined entry + +def parse_entry(ls): + command = ls[0] + if command not in PARSE_DEFS: + error("Unknown command: "+command) + return (None,None) + parse_def = PARSE_DEFS[command] + p = [] + for pd in parse_def: + ls = ls[1:] + if len(ls) < 1: + parse_error(PARSE_DEF_NAME[pd]+" expected: (end of line)") + return (None,None) + elif pd == PARSE_KEY: + if not is_key(ls[0]): + parse_error(PARSE_DEF_NAME[pd]+" expected: "+ls[0]) + return (None,None) + p.append(ls[0]) + elif pd == PARSE_INT: + try: + value = int(ls[0]) + except ValueError: + parse_error(PARSE_DEF_NAME[pd]+" expected: "+ls[0]) + return (None,None) + p.append(value) + elif pd == PARSE_STR: + p.append(ls[0]) + elif pd == PARSE_KEYS: + while len(ls) > 0: + if not is_key(ls[0]): + parse_error(PARSE_DEF_NAME[pd]+" expected: "+ls[0]) + return (None,None) + p.append(ls[0]) + ls = ls[1:] + return (command,tuple(p)) + +# validate key references + +def check_list(list_key,key=None): # key=None to just check list + for i in range(len(defs_list)): + if defs_list[i][0] == list_key: + if key == None: + return (i,None) + for j in range(1,len(defs_list[i])): + if defs_list[i][j] == key: + return (i,j-1,len(defs_list[i])-1) + parse_error("KEY not found in LIST("+list_key+"): "+key) + return (None,None,None) + parse_error("LIST not found: "+list_key) + return (None,None,None) + +def check_setgroup(key): + for i in range(len(defs_setgroup)): + if defs_setgroup[i][0] == key: return i + parse_error("SETGROUP not found: "+key) + return None + +def check_set(group_key,key): + for i in range(len(defs_setgroup)): + if defs_setgroup[i][0] == group_key: + for j in range(len(defs_set)): + if defs_set[j][0] == i and defs_set[j][1] == key: + return (i,j) + parser_error("SET not found in SETGROUP("+group_key+"): "+key) + return (None,None) + parse_error("SETGROUP not found: "+group_key) + return (None,None) + +def check_prop(key): + for i in range(len(defs_prop)): + if defs_prop[i][0] == key: + return i + parse_error("PROP not found: "+key) + return None + +def check_songprop(key): + for i in range(len(defs_songprop)): + if defs_songprop[i][0] == key: + return i + parse_error("SONGPROP not found: "+key) + return None + +def check_unit(key): + for i in range(len(defs_unit)): + if defs_unit[i] == key: return i + parse_error("UNIT not found: "+key) + return None + +def check_channel(unit_key,key): + for i in range(len(defs_unit)): + if defs_unit[i] == unit_key: + for j in range(len(defs_channel)): + if defs_channel[j][0] == i and defs_channel[j][1] == key: + return (i,j) + parser_error("CHANNEL not found in UNIT("+unit_key+"): "+key) + return (None,None) + parse_error("UNIT not found: "+unit_key) + return (None,None) + +# parse enums file + +def parse_enums(path): + global defs_list, defs_setgroup, defs_set, defs_prop, defs_songprop, defs_unit, defs_channel, defs_channelonlist, defs_local, defs_localdefault + global parse_path, parse_line + print("Parsing: " + path) + parse_path = path + parse_line = 0 + localcurrent = None + for l in open(path,"rt",encoding="utf-8").readlines(): + parse_line += 1 + ls = shlex.split(l,comments=True) + if len(ls) < 1: + continue + (command,p) = parse_entry(ls) + if command == None: continue + verbose(command + ": " + str(p)) + if command == "LIST": defs_list.append(p) + elif command == "SETGROUP": defs_setgroup.append(p) + elif command == "SETINT": # 0-group 1-key 2-default 3-min 4-max + gi = check_setgroup(p[0]) + if gi != None: + defs_set.append((gi,p[1],p[2],p[3],p[4],None,False)) # set: group, key, default, int min, int max, list, is_string + elif command == "SETLIST": # 0-group 1-key 2-list 3-default + gi = check_setgroup(p[0]) + if gi != None: + (li,dv,dcount) = check_list(p[2],p[3]) + if li != None: + defs_set.append((gi,p[1],dv,0,dcount-1,li,False)) + elif command == "SETSTR": # 0-group 1-key 2-default + gi = check_setgroup(p[0]) + if gi != None: + defs_set.append((gi,p[1],p[2],0,0,None,True)) + elif command == "PROPINT": defs_prop.append((p[0],PROP_INT)) + elif command == "PROPLONG": defs_prop.append((p[0],PROP_LONG)) + elif command == "PROPSTR": defs_prop.append((p[0],PROP_STR)) + elif command == "PROPLINES": defs_prop.append((p[0],PROP_LINES)) + elif command == "PROPBLOB": defs_prop.append((p[0],PROP_BLOB)) + elif command == "SONGPROPINT": defs_songprop.append((p[0],PROP_INT)) + elif command == "SONGPROPLONG": defs_songprop.append((p[0],PROP_LONG)) + elif command == "SONGPROPSTR": defs_songprop.append((p[0],PROP_STR)) + elif command == "SONGPROPLINES": defs_songprop.append((p[0],PROP_LINES)) + elif command == "SONGPROPBLOB": defs_songprop.append((p[0],PROP_BLOB)) + elif command == "UNIT": defs_unit.append(p[0]) + elif command == "CHANNEL": + ui = check_unit(p[0]) + if ui != None: + defs_channel.append((ui,p[1])) + elif command == "CHANNELONLIST": + (li,ki) = check_list(p[0]) + if li != None: + if defs_channelonlist != None: + parse_error("CHANNELONLIST already used: "+defs_list[defs_channelonlist][0]) + else: + defs_channelonlist = li + elif command == "LOCAL": + defs_local.append([p[0],p[1],[],[],[],[],[],[],[],None,[]]) # local: key, name, list, group, set, prop, songprop, unit, channel, channelset,text + localcurrent = len(defs_local)-1 + elif command == "LOCALDEFAULT": + if localcurrent == None: parse_error("LOCAL must be used before "+command) + elif (defs_localdefault != None): + parse_error("LOCALDEFAULT already used: "+defs_local[defs_localdefault][0]) + else: + defs_localdefault = localcurrent + elif command == "LOCALLIST": + if localcurrent == None: parse_error("LOCAL must be used before "+command) + else: + (li,lk,lcount) = check_list(p[0],p[1]) + if li != None: + defs_local[localcurrent][2].append((li,lk,p[2])) # list, key, name + elif command == "LOCALSETGROUP": + if localcurrent == None: parse_error("LOCAL must be used before "+command) + else: + gi = check_setgroup(p[0]) + if gi != None: + defs_local[localcurrent][3].append((gi,p[1],p[2])) # group, name, desc + elif command == "LOCALSET": + if localcurrent == None: parse_error("LOCAL must be used before "+command) + else: + (gi,si) = check_set(p[0],p[1]) + if gi != None: + defs_local[localcurrent][4].append((gi,si,p[1],p[2])) # group, set, name, desc + elif command == "LOCALPROP": + if localcurrent == None: parse_error("LOCAL must be used before "+command) + else: + pi = check_prop(p[0]) + if pi != None: + defs_local[localcurrent][5].append((pi,p[1])) # prop, name + elif command == "LOCALSONGPROP": + if localcurrent == None: parse_error("LOCAL must be used before "+command) + else: + pi = check_songprop(p[0]) + if pi != None: + defs_local[localcurrent][6].append((pi,p[1])) # songprop, name + elif command == "LOCALUNIT": + if localcurrent == None: parse_error("LOCAL must be used before "+command) + else: + ui = check_unit(p[0]) + if ui != None: + defs_local[localcurrent][7].append((ui,p[1],p[2])) # unit, name, desc + elif command == "LOCALCHANNEL": + if localcurrent == None: parse_error("LOCAL must be used before "+command) + else: + (ui,ci) = check_channel(p[0],p[1]) + if ui != None: + defs_local[localcurrent][8].append((ci,p[2])) # channel, name + elif command == "LOCALCHANNELSET": + if localcurrent == None: parse_error("LOCAL must be used before "+command) + else: + if defs_local[localcurrent][9] != None: + parse_error("LOCALCHANNELSET already used") + else: + defs_local[localcurrent][9] = list(p) + elif command == "LOCALTEXT": + if localcurrent == None: parse_error("LOCAL must be used before "+command) + else: + defs_local[localcurrent][10].append(p) + elif command == "LOCALERROR": + if localcurrent == None: parse_error("LOCAL must be used before "+command) + else: + defs_local[localcurrent][10].append(("ERROR_"+p[0],p[1])) + parse_path = None + +def parse_enum_files(files): + global defs_localdefault, defs_local + defs_localdefault = None + for p in files: parse_enums(p) + # move localdefault to the start of defs_local + if defs_localdefault == None: fatal_error("No LOCALDEFAULT found") + defs_local = [defs_local[defs_localdefault]] + defs_local[0:defs_localdefault] + defs_local[defs_localdefault+1:] + localefault = None + # ensure other important things are present + if defs_channelonlist == None: fatal_error("No CHANNELONLIST found") + +# generate enums data + +gen_enum_lines = None +gen_data_lines = None + +gen_text_blob = None +gen_text_map = None + +def gen_line(l,target=0): + global gen_enum_lines, gen_data_lines + verbose(("%d:" % (target)) + l) + if target == 1: gen_data_lines.append(l) + else: gen_enum_lines.append(l) + +def gen_break(target=0): + gen_line("",target) + +def gen_enum(key,value,target=0,hexadecimal=False): + if not hexadecimal: gen_line("#define %-40s %12d" % (key,value),target) + else: gen_line("#define %-40s %12X" % (key,value),target) + +def gen_text(text): # adds utf-8 string to text blob, returns offset to it, duplicates are reused + global gen_text_blob, gen_text_map + if text in gen_text_map: + return gen_text_map[text] + offset = len(gen_text_blob) + gen_text_blob += text.encode("utf-8") + gen_text_blob.append(0) + gen_text_map[text] = offset + return offset + +def gen_data(data,mode=0,prefix="\t",target=1): # mode: 0=hex bytes (2-digit), 1=int bytes (3-digit), 2=int 4-digit, 3=hex 24-bit (6-digit) + i=0 + c=0 + s="" + FORMS = ["0x%02X,","%3d,","%4d,","0x%06X,"] + COLUMNS = [ 32, 32, 24, 16 ] + columns = COLUMNS[mode] + form = FORMS[mode] + for i in range(len(data)): + s += form % (data[i]) + i += 1 + c += 1 + if (c >= columns): + gen_line(prefix+s,target) + s = "" + c = 0 + if len(s) > 0: + gen_line(prefix+s,target) + +def generate_enums(file_enum,file_data,do_write): + global gen_enum_lines, gen_data_lines + global gen_text_blob, gen_text_map + global defs_list, defs_setgroup, defs_set, defs_local + locs = len(defs_local) + gen_enum_lines = [] # public interface enums + gen_data_lines = [] # internal data tables + gen_text_blob = bytearray() + gen_text_map = {} + print("Generating data...") + for i in range(2): gen_line("#pragma once",i) + for i in range(2): gen_line("// generated by "+os.path.basename(__file__),i) + for i in range(2): gen_line("// "+now_string,i) + for i in range(2): gen_break(i) + gen_line("#include \"" + OUTPUT_ENUM + "\"",1) + gen_break(1) + # + # locale tables contain a byte index to every generated string in gen_data_blob + # defs_local: key, name, list, group, set, prop, songprop, unit, channel, channelset,text + # + table_locale = [[]] * locs + # + # generate list data + # + # generate list for locales and corresponding setting in main group + LOCALE_KEY = "LOCALE" + list_locale = [LOCALE_KEY] + for l in defs_local: list_locale.append(l[0]) + list_locale_index = len(defs_list) + defs_list.append(list_locale) + defs_set.append((0,LOCALE_KEY,0,0,locs-1,list_locale_index,False)) # SETLIST: group-0, LOCALE_KEY, default, min, max, list, not-string + for i in range(len(defs_local)): # create the list names in each locale + for j in range(len(defs_local)): + defs_local[i][2].append((list_locale_index,j,defs_local[j][1])) # LOCALLIST: LOCALE_KEY, locale-key, locale-name + # map each list to a text index, and gather the locale strings to go with it + gen_enum("NSFP_LIST_COUNT",len(defs_list)); + table_list = [] + for li in range(len(defs_list)): + list_key = defs_list[li][0] + keys = list(defs_list[li][1:]) + gen_enum("NSFP_LIST_"+list_key,len(table_list)) + table_list.append(len(table_locale[0])) # locale table index used by table values + names = [keys[:] for i in range(locs)] # keys used as fallback if no default locale + for i in range(locs): + for j in range(len(keys)): + name = None + mapped = False + for (lli,lki,lname) in defs_local[i][2]: + if lli == li and lki == j: + mapped = True + name = lname + break + if not mapped: warn("LOCAL("+defs_local[i][0]+") missing: LOCALLIST "+list_key+" "+keys[j]+" *") + if name == "*": name = None + if name != None: names[i][j] = name + if i == 0: # default locale used as fallback + for k in range(1,locs): + names[k][j] = names[0][j] + # the list combines into a multi-string with 0 terminators in between + local_list = "" + for j in range(len(keys)): + local_list += names[i][j] + "\0" + table_locale[i].append(gen_text(local_list)) + gen_break(0) + gen_line("const int32_t NSFPD_LIST[NSFP_LIST_COUNT] = {",1) + gen_data(table_list,mode=2) + gen_line("};",1) + gen_break(1) + # + # generate group data + # + # generate groups for units + gen_enum("NSFP_UNIT_COUNT",len(defs_unit)) + for ui in range(len(defs_unit)): + unit_key = defs_unit[ui] + gen_enum("NSFP_UNIT_"+unit_key,ui) + gi = len(defs_setgroup) + defs_setgroup.append([unit_key]) + for i in range(locs): + mapped = False + for (lui,name,desc) in defs_local[i][7]: + if lui == gi: + defs_local[i][3].append((gi,name,desc)) + mapped = True + break + if not mapped: + warn("LOCAL("+defs_local[i][0]+") missing: LOCALUNIT "+unit_key+" * *") + defs_local[i][3].append((gi,"*","*")) # suppress group warning + gen_break(0) + # map each group to a text index, gather locale strings + gen_enum("NSFP_GROUP_COUNT",len(defs_setgroup)) + gen_line("typedef struct {",1) + gen_line("\tconst char* key;",1) + gen_line("\tint32_t text;",1) + gen_line("} NSFSetGroupData;",1) + gen_line("const NSFSetGroupData NSFPD_GROUP[NSFP_GROUP_COUNT] = {",1) + for gi in range(len(defs_setgroup)): + group_key = defs_setgroup[gi][0] + gen_enum("NSFP_GROUP_"+group_key,gi) + gen_line("\t{ %30s,%4d }," % ('"'+group_key+'"',len(table_locale[0])),1) + names = [group_key for i in range(locs)] + descs = [group_key for i in range(locs)] + for i in range(locs): + name = None + desc = None + mapped = False + for (lgi,lname,ldesc) in defs_local[i][3]: + if lgi == gi: + mapped = True + name = lname + desc = ldesc + break + if not mapped: warn("LOCAL("+defs_local[i][0]+") missing: LOCALSETGROUP "+group_key+" * *") + if name == "*": name = None + if desc == "*": desc = None + if name != None: names[i] = name + if desc != None: descs[i] = desc + if i == 0: + for j in range(1,locs): + names[j] = names[0] + descs[j] = descs[0] + table_locale[i].append(gen_text(names[i])) + table_locale[i].append(gen_text(descs[i])) + gen_break(0) + gen_line("};",1) + gen_break(1) + # + # generate setting data + # + # generate settings for channels + CHANNEL_ADD = ("ON","VOL","PAN") + gen_enum("NSFP_CHANNEL_COUNT",len(defs_channel)) + gen_line("typedef struct {",1) + gen_line("\tconst char* key;",1) + gen_line("\tint32_t unit, text;",1) + gen_line("} NSFChannelData;",1) + gen_line("const NSFChannelData NSFPD_CHANNEL[NSFP_CHANNEL_COUNT] = {",1) + for i in range(locs): + if defs_local[i][9] == None: + warn("LOCAL("+defs_local[i][0]+") missing: LOCALCHANNELSET * * * * * *") + defs_local[i][9] = ["*","*","*","*","*","*"] + for j in range(6): + if defs_local[i][9][j] == "*": + if i==0: defs_local[i][9][j] = CHANNEL_ADD[i%3] # default locale defers to key + else: defs_local[i][9][j] = defs_local[0][9][j] # defer to default locale + for ci in range(len(defs_channel)): + ui = defs_channel[ci][0] + unit_key = defs_unit[ui] + gi = check_setgroup(unit_key) + channel_key = defs_channel[ci][1] + si = len(defs_set) + gen_line("\t{ %30s,%2d,%4d }," % ('"'+channel_key+'"',ui,len(table_locale[0])),1) + gen_enum("NSFP_CHANNEL_"+unit_key+"_"+channel_key,ci) + defs_set.append((gi,channel_key+"_"+CHANNEL_ADD[0],1,0,1,defs_channelonlist,False)) + defs_set.append((gi,channel_key+"_"+CHANNEL_ADD[1],500,0,1000,None,False)) + defs_set.append((gi,channel_key+"_"+CHANNEL_ADD[2],500,0,1000,None,False)) + names = [channel_key for i in range(locs)] + for i in range(locs): + name = None + mapped = False + for j in range(len(defs_local[i][8])): + if (defs_local[i][8][0] == ci): + mapped = True + name = defs_local[i][8][1] + if not mapped: warn("LOCAL("+defs_local[i][0]+") missing: LOCALCHANNEL "+unit_key+" "+channel_key+" *") + if name == "*": name = None + if name != None: names[i] = name + if i == 0: + for j in range(1,locs): + names[j] = names[0] + table_locale[i].append(gen_text(names[i])) + for j in range(3): + defs_local[i][4].append((gi,si+j,names[i]+defs_local[i][9][j+0],names[i]+defs_local[i][9][j+3])) + gen_break(0); + gen_line("};",1) + gen_break(1) + # map each setting to a text index, gather locale strings + gen_enum("NSFP_SET_COUNT",len(defs_set)) + gen_line("typedef struct {",1) + gen_line("\tconst char* key;",1) + gen_line("\tint32_t group, text;",1) + gen_line("\tint32_t default_int, min_int, max_int, list;",1) + gen_line("\tconst char* default_str;",1) + gen_line("} NSFSetData;",1) + gen_line("const NSFSetData NSFPD_SET[NSFP_SET_COUNT] = {",1) + for si in range(len(defs_set)): + gi = defs_set[si][0] + group_key = defs_setgroup[gi][0] + set_key = defs_set[si][1] + default_int = 0 + default_str = "NULL" + if defs_set[si][6]: + default_str = "\""+defs_set[si][2]+"\"" + else: + default_int = defs_set[si][2] + if (default_int < defs_set[si][3]) or (default_int > defs_set[si][4]): + error("SET %s %s default out of range: %d <= %d <= %d" % (group_key,set_key,defs_set[si][3],default_int,defs_set[si][4])) + list_index = -1 + if defs_set[si][5] != None: list_index = defs_set[si][5] + gen_line("\t{ %30s,%3d,%4d,%7d,%7d,%7d,%3d,%s }," % ('"'+set_key+'"',gi,len(table_locale[0]),default_int,defs_set[si][3],defs_set[si][4],list_index,default_str),1) + gen_enum("NSFP_SET_"+group_key+"_"+set_key,si) + names = [set_key for i in range(locs)] + descs = [set_key for i in range(locs)] + for i in range(locs): + name = None + desc = None + mapped = False + for (lgi,lsi,lname,ldesc) in defs_local[i][4]: + if lgi == gi and lsi == si: + mapped = True + name = lname + desc = ldesc + break + if not mapped: warn("LOCAL("+defs_local[i][0]+") missing: LOCALSET "+group_key+" "+set_key+" * *") + if name == "*": name = None + if desc == "*": desc = None + if name != None: names[i] = name + if desc != None: descs[i] = desc + if i == 0: + for j in range(1,locs): + names[j] = names[0] + descs[j] = descs[0] + table_locale[i].append(gen_text(names[i])) + table_locale[i].append(gen_text(descs[i])) + gen_break(0) + gen_line("};",1) + gen_break(1) + # + # generate prop data + # + gen_enum("NSFP_PROP_COUNT",len(defs_prop)) + gen_line("typedef struct {",1) + gen_line("\tconst char* key;",1) + gen_line("\tint32_t type, text;",1) + gen_line("} NSFPropData;",1) + gen_line("const NSFPropData NSFPD_PROP[NSFP_PROP_COUNT] = {",1) + for pi in range(len(defs_prop)): + prop_key = defs_prop[pi][0] + gen_line("\t{ %30s,%1d,%4d }," % ('"'+prop_key+'"',defs_prop[pi][1],len(table_locale[0])),1) + gen_enum("NSFP_PROP_"+prop_key,pi); + names = [prop_key for i in range(locs)] + for i in range(locs): + name = None + mapped = False + for (lpi,lname) in defs_local[i][5]: + if lpi == pi: + mapped = True + name = lname + break + if not mapped: warn("LOCAL("+defs_local[i][0]+") missing: LOCALPROP "+prop_key+" *") + if name == "*": name = None + if name != None: names[i] = name + if i == 0: + for j in range(1,locs): + names[j] = names[0] + table_locale[i].append(gen_text(names[i])) + gen_break(0) + gen_line("};",1) + gen_break(1) + # + # generate songprop data + # + gen_enum("NSFP_SONGPROP_COUNT",len(defs_songprop)) + gen_line("typedef struct {",1) + gen_line("\tconst char* key;",1) + gen_line("\tint32_t type, text;",1) + gen_line("} NSFSongPropData;",1) + gen_line("const NSFSongPropData NSFPD_SONGPROP[NSFP_SONGPROP_COUNT] = {",1) + for pi in range(len(defs_songprop)): + prop_key = defs_songprop[pi][0] + gen_line("\t{ %30s,%1d,%4d }," % ('"'+prop_key+'"',defs_songprop[pi][1],len(table_locale[0])),1) + gen_enum("NSFP_SONGPROP_"+prop_key,pi); + names = [prop_key for i in range(locs)] + for i in range(locs): + name = None + mapped = False + for (lpi,lname) in defs_local[i][6]: + if lpi == pi: + mapped = True + name = lname + break + if not mapped: warn("LOCAL("+defs_local[i][0]+") missing: LOCALSONGPROP "+prop_key+" *") + if name == "*": name = None + if name != None: names[i] = name + if i == 0: + for j in range(1,locs): + names[j] = names[0] + table_locale[i].append(gen_text(names[i])) + gen_break(0) + gen_line("};",1) + gen_break(1) + # + # generate extra text + # + table_text = [] + gen_enum("NSFP_TEXT_COUNT",len(defs_local[0][10])) + for ti in range(len(defs_local[0][10])): + text_key = defs_local[0][10][ti][0] + gen_enum("NSFP_TEXT_"+text_key,ti) + names = [text_key for i in range(locs)] + table_text.append(len(table_locale[0])) + for i in range(locs): + name = None + mapped = False + for (lti,lname) in defs_local[i][10]: + if lti == ti: + mapped = True + name = lname + break + if not mapped: warn("LOCAL("+defs_local[i][0]+") missing: LOCALTEXT "+text_key+" *") + if name == "*": name = None + if name != None: names[i] = name + table_locale[i].append(gen_text(names[i])) + gen_break(0) + gen_line("const int32_t NSFPD_TEXT[NSFP_LIST_COUNT] = {",1) + gen_data(table_text,mode=2) + gen_line("};",1) + gen_break(1) + # verify there aren't stray TEXT definitions outside the default locale + for i in range(1,locs): + for (text_key,text_name) in defs_local[i][10]: + mapped = False + for j in range(len(defs_local[0][10])): + if text_key == defs_local[0][10][j][0]: + mapped = True + break + if not mapped: error("LOCAL("+defs_local[i][0]+") LOCALTEXT not in default LOCAL: "+text_key) + # + # generate text data tables + # + gen_enum("NSFP_LOCALE_COUNT",len(defs_local)) + gen_line("const int32_t NSFPD_LOCAL_TEXT[NSFP_LOCALE_COUNT][%d] = {" % (len(table_locale[0])),1) + for i in range(0,locs): + gen_enum("NSFP_LOCALE_"+defs_local[i][0],i) + gen_line("{",1) + gen_data(table_locale[i],mode=3) + gen_line("},",1) + gen_break(0); + gen_line("};",1) + gen_break(1); + gen_line("const uint8_t NSFPD_LOCAL_TEXT_DATA[0x%06X] = {" % (len(gen_text_blob)),1) + gen_data(gen_text_blob,mode=0) + gen_line("};",1) + gen_break(1); + # + # save the data + # + for i in range(2): gen_line("// end of file",i) + if (do_write): + print("Writing: "+file_enum) + with open(file_enum,"wt") as f: + for l in gen_enum_lines: f.write(l+"\n") + print("Writing: "+file_data) + with open(file_data,"wt") as f: + for l in gen_data_lines: f.write(l+"\n") + print("Generate finished.") + +# verify generated enums data + +def ignore_line(l): + ls = l.strip() + if len(ls) < 1: return True + if ls.startswith("//"): return True + if ls.startswith("#pragma"): return True + if ls.startswith("#include"): return True + return False + +def verify_lines(lines0,file): + global parse_path, parse_line + print("Verify: "+file) + parse_path = file + lines1 = open(file,"rt").readlines() + i0 = 0 + i1 = 0 + while i0 < len(lines0) and i1 < len(lines1): + if ignore_line(lines0[i0]): + i0 += 1 + continue + if ignore_line(lines1[i1]): + i1 += 1 + continue + if lines0[i0].strip() != lines1[i1].strip(): + parse_line = i1 + 1 + parse_error("Verify failed (generated:%d)" % (i0+1)) + parse_path = None + return + i0 += 1 + i1 += 1 + while i1 < len(lines1): + if not ignore_line(lines1[i1]): + parse_line = i1 + 1 + parse_error("Verify failed, extra line") + parse_path = None + return + i1 += 1 + parse_path = None + while i0 < len(lines0): + if not ignore_line(lines0[i0]): + error("Verify failed, extra line at generated:%d" % (i0+1)) + return + i0 += 1 + return + +def verify_enums(file_enum,file_data): + verify_lines(gen_enum_lines,file_enum) + verify_lines(gen_data_lines,file_data) + +# +# main +# + +if __name__ == "__main__": + ap = argparse.ArgumentParser() + ap.add_argument("-verify",action="store_true",help="Suppress output, but verify match with existing generated files.") + ap.add_argument("-strict",action="store_true",help="Treat warnings as errors.") + ap.add_argument("-nowarn",action="store_true",help="Disable all warnings.") + ap.add_argument("-verbose",action="store_true",help="Verbose diagnostic output.") + args = ap.parse_args() + VERIFY = args.verify + STRICT = args.strict + NOWARN = args.nowarn + VERBOSE = args.verbose + +if VERIFY: print("Option: -verify") +if STRICT: print("Option: -strict") +if NOWARN: print("Option: -nowarn") +if VERBOSE: print("Option: -verbose") + +input_files = [os.path.join(INPUT_FOLDER,INPUT_FIRST)] +for p in sorted(os.listdir(INPUT_FOLDER)): + if (p.lower().endswith(".txt") and p != INPUT_FIRST): + input_files.append(os.path.join(INPUT_FOLDER,p)) + +print("Parsing %d files..." % (len(input_files))) +parse_enum_files(input_files) + +generate_enums(OUTPUT_ENUM,OUTPUT_DATA,not VERIFY) +verify_enums(OUTPUT_ENUM,OUTPUT_DATA) + +terminate() diff --git a/enums/settings.txt b/enums/settings.txt new file mode 100644 index 0000000..bdf1397 --- /dev/null +++ b/enums/settings.txt @@ -0,0 +1,31 @@ + +LIST ENABLE OFF ON +CHANNELONLIST ENABLE + +SETGROUP MAIN +SETINT MAIN VOLUME 500 0 1000 +SETINT MAIN SAMPLERATE 48000 1000 4000000 +SETLIST MAIN STEREO ENABLE ON +SETSTR MAIN TITLE_FORMAT "%L (%n/%e) %T - %A" + +PROPINT SONGCOUNT +PROPLONG LONG +PROPSTR TITLE +PROPLINES INFO +PROPBLOB BLOB + +SONGPROPINT INT +SONGPROPINT SONGTEST +SONGPROPLONG LONG +SONGPROPSTR TITLE +SONGPROPLINES INFO +SONGPROPBLOB BLOB + +UNIT APU0 +CHANNEL APU0 SQU0 +CHANNEL APU0 SQU1 + +UNIT APU1 +CHANNEL APU1 TRI +CHANNEL APU1 NSE +CHANNEL APU1 DPCM diff --git a/gui/gui.cpp b/gui/gui.cpp index 5680bca..b5a4768 100644 --- a/gui/gui.cpp +++ b/gui/gui.cpp @@ -1,8 +1,3 @@ // stub #include - -int nsfplaygui::test() -{ - return 4; -} diff --git a/gui/gui.vcxproj b/gui/gui.vcxproj index 44030f7..2832592 100644 --- a/gui/gui.vcxproj +++ b/gui/gui.vcxproj @@ -178,6 +178,8 @@ + + diff --git a/gui/gui.vcxproj.filters b/gui/gui.vcxproj.filters index d264bda..e434290 100644 --- a/gui/gui.vcxproj.filters +++ b/gui/gui.vcxproj.filters @@ -18,6 +18,12 @@ Header Files + + Header Files + + + Header Files + diff --git a/include/nsfplaycore.h b/include/nsfplaycore.h index bda0a6d..b9b090e 100644 --- a/include/nsfplaycore.h +++ b/include/nsfplaycore.h @@ -1,9 +1,229 @@ -// stub +#pragma once -// this is the public interface to the core library -namespace nsfplaycore +// NSFPlay core library public interface + +// General notes: +// - NSFCore assumes single threaded access, do not call multiple core state modifying functions concurrently. +// - All strings are UTF-8. +// - For all functions that return a const char*, this will point to an internal temporary buffer +// which is only valid until the next nsfplay call that returns a const char*. +// - The song index begins at 0 for the interface, but when presented to the user a +1 should be used, +// so that the first song in the NSF is song 1. + +#include + +// auto-generated enumerations for settings and properties +#include "nsfplayenums.h" + +extern "C" +{ + +// hidden implementation +struct NSFCore; + + +typedef struct +{ + int32_t index; + int32_t val_int; + const char* val_str; // NULL if not string +} NSFSetInit; + +// create or destroy an core instance +// - ini_data is a null terminated string, containing ini file settings +// - a NULL ini_data will use the default settings +// - and init array can be used instead of ini, terminated with an entry with index -1. +NSFCore* nsfplay_create(const char* ini_data); +NSFCore* nsfplay_create_init(const NSFSetInit* init); +void nsfplay_destroy(NSFCore* core); + +// reset all settings to default values +void nsfplay_set_default(NSFCore* core); + +// returns a localized string describing the last error +// - NULL if there has been no logged error since the last call to nsfplay_test_error +// - once returned, the error state will be cleared, and subsequent calls will return NULL until another error is caught +const char* nsfplay_last_error(NSFCore* core); + +// settings by index +// - use the provided NSFP_SET_x enumerations, because index values are subject to change +// - set returns false if index is out of bounds, or the wrong value type was used +// - get returns 0 or NULL if out of bounds, or wrong type +bool nsfplay_set_int(NSFCore* core, int32_t index, int32_t value); +bool nsfplay_set_str(NSFCore* core, int32_t index, const char* value); +int32_t nsfplay_get_int(const NSFCore* core, int32_t index); +const char* nsfplay_get_str(const NSFCore* core, int32_t index); + +// settings by string key +// - use nsfplay_setting_index +bool nsfplay_set_key_int(NSFCore* core, const char* key, int32_t value); +bool nsfplay_set_key_str(NSFCore* core, const char* key, const char* value); +int32_t nsfplay_get_key_int(const NSFCore* core, const char* key); +const char* nsfplay_get_key_str(const NSFCore* core, const char* key); + +// ini file +// - returns false if any lines could not be parsed +// - settings are separated by line endings, any combination of cr and/or lf is accepted +// - blank lines are ignored, anything after # will be ignored until the next line +bool nsfplay_ini(NSFCore* core, const char* ini_data); +// read a current setting as an ini line +// - does not include newline +// - iterate from 0 to NSFP_SET_COUNT-1 to generate a complete ini file +const char* nsfplay_ini_line(const NSFCore* core, int32_t index); + +// information about settings, useful for UI display + +typedef struct +{ + // all const char* in this structure point to static strings, permanently available + int32_t group; + const char* key; // ini text key + const char* name; // localized name, according to current language setting + const char* description; // localized description, according to current language setting + bool is_string; + int32_t default_int; // 0 if is_string + int32_t min_int; + int32_t max_int; + const char* list; // if not NULL, contains a series of (1+max_int) localized null terminated strings naming each option + const char* default_str; // NULL only if !is_string +} NSFSetInfo; + +typedef struct +{ + const char* key; // not used by ini, but does give a non-localized permanent string key for this group + const char* name; // localized name + const char* description; // localized description +} NSFSetGroupInfo; + +NSFSetInfo nsfplay_set_info(int32_t index); +NSFSetGroupInfo nsfplay_set_group_info(int32_t group); +int32_t nsfplay_set_key_index(const char* key); // -1 if not found +int32_t nsfplay_set_group_index(const char* key); // -1 if not found + + +// load/change the current NSF file +// - load makes an internal copy of nsf_data +// - assume will instead use nsf_data directly, assuming the pointer will remain valid +// - returns false if nsf_data could not be parsed +// - a NULL nsf_data may be used to "unload" the current NSF, +// but even without an NSF, the core may still be used via direct emulation access +bool nsfplay_load(NSFCore* core, const void* nsf_data, uint32_t nsf_size); +bool nsfplay_assume(NSFCore* core, const void* nsf_data, uint32_t nsf_size); + + +// convenience function for time conversion +uint64_t nsfplay_time_to_samples(const NSFCore* core, int32_t hours, int32_t minutes, int32_t seconds, int32_t milliseconds); +void nsfplay_samples_to_time(const NSFCore* core, uint64_t samples, int32_t* hours, int32_t* minutes, int32_t* seconds, int32_t* milliseconds); + +// song control +uint32_t nsfplay_song_count(const NSFCore* core); // number of songs in loaded NSF +bool nsfplay_song(NSFCore* core, uint8_t song); // set song, false if song out of bounds, automatically calls song_play +void nsfplay_song_play(NSFCore* core); // resets the song and executes its INIT routine +void nsfplay_seek(NSFCore* core, uint64_t samples); +uint64_t nsfplay_samples_played(const NSFCore* core); // samples since song_play + +// advance emulation and render sound samples +// - every two elements of stereo_output alternates left channel, right channel (length must be 2*samples) +// - stereo_output can be NULL if the output isn't needed +// - returns number of samples rendered, may be less than samples if song is finished (will zero fill unused output samples) +uint32_t nsfplay_render(NSFCore* core, uint32_t samples, int16_t* stereo_output); + + +// direct emulation access +uint8_t nsfplay_emu_peek(const NSFCore* core, uint16_t address); // peek at memory, no read side effects +uint8_t nsfplay_emu_read(NSFCore* core, uint16_t address); // read memory, some registers have read side effects +void nsfplay_emu_poke(NSFCore* core, uint16_t address, uint8_t value); // write to memory +void nsfplay_emu_reg_set(NSFCore* core, char reg, uint16_t value); // reg is one of A, X, Y, S, P (flags), * (PC) +uint16_t nsfplay_emu_reg_get(const NSFCore* core, char reg); +void nsfplay_emu_run(NSFCore* core, uint32_t cycles); +uint32_t nsfplay_emu_samples_pending(const NSFCore* core); // number of sound samples due to emu_run that have not been rendered out yet +void nsfplay_emu_cancel_pending(NSFCore* core); +uint64_t nsfplay_emu_cycles(const NSFCore* core); // cycles since song_play +uint32_t nsfplay_emu_cycles_to_next_sample(const NSFCore* core); // cycles until next pending sample is generated + + +// NSF properties +// - prop parameter should use the NSFP_PROP_key enumerations, as the values are subject to change +// - song prop uses NSFP_SONG_PROP_key enumerations +// - NSFP_PROP_COUNT and NSFP_SONG_PROP_COUNT can be used iterate over all properties + +#define NSFP_PROP_TYPE_INVALID 0 +#define NSFP_PROP_TYPE_INT 1 +#define NSFP_PROP_TYPE_LONG 2 +#define NSFP_PROP_TYPE_STR 3 +#define NSFP_PROP_TYPE_LINES 4 +#define NSFP_PROP_TYPE_BLOB 5 +typedef struct +{ + const char* key; // permanent string ID + const char* name; // localized name + int32_t type; +} NSFPropInfo; + +NSFPropInfo nsfplay_prop_info(const NSFCore* core, int32_t prop); // type will be INVALID if not present +NSFPropInfo nsfplay_prop_song_info(const NSFCore* core, int32_t song, int32_t prop); + +int32_t nsfplay_prop_int(const NSFCore* core, int32_t prop); +int64_t nsfplay_prop_long(const NSFCore* core, int32_t prop); +const char* nsfplay_prop_str(const NSFCore* core, int32_t prop); // NULL if not found or wrong type +int32_t nsfplay_prop_lines(const NSFCore* core, int32_t prop); // returns line count (-1 if not found), prepares for first prop_line +const char* nsfplay_prop_line(const NSFCore* core); // returns next line (NULL if no more lines) +const void* nsfplay_prop_blob(const NSFCore* core, uint32_t* blob_size); // blob_size written if not NULL + +int32_t nsfplay_prop_song_type(const NSFCore* core, int32_t song, int32_t prop); +int32_t nsfplay_prop_song_int(const NSFCore* core, int32_t song, int32_t prop); +int64_t nsfplay_prop_song_long(const NSFCore* core, int32_t song, int32_t prop); +const char* nsfplay_prop_song_str(const NSFCore* core, int32_t song, int32_t prop); +int32_t nsfplay_prop_song_lines(const NSFCore* core, int32_t song, int32_t prop); // prepares nsfplay_prop_line +const void* nsfplay_prop_song_blob(const NSFCore* core, int32_t song, uint32_t* blob_size); + +// NSFe or NSF2 chunks can be fetched for manual inspection +// - fourcc does not need a terminating 0, only the first 4 characters will be used +// - chunk_size will be written if not NULL +const void* nsfplay_chunk(const NSFCore* core, const char* fourcc, uint32_t* chunk_size); + + +// Channel info + +typedef struct // NSFP_UNIT_key (< NSFP_UNIT_COUNT) +{ + const char* key; // permanent string ID + const char* name; // localized name + bool active; // whether this unit is active for this NSF +} NSFChannelUnit; + +typedef struct // NSFP_CHANNEL_key (< NSFP_CHANNEL_COUNT) { + int32_t unit; // unit + const char* key; // permanent string ID + const char* name; // localized name +} NSFChannelInfo; + +typedef struct +{ + int32_t channel; // global channel index (-1 if invalid) + float frequency; // calculated expected frequency + int32_t volume; // volume, a register value or 0 if muted + uint32_t reg_period; // register period or frequency value + uint32_t reg_volume; // register volume valud + uint32_t reg_extra[3]; // extra data (channel specific) +} NSFChannelState; + +// every channel of every expansion audio unit has a unique global index +NSFChannelUnit nsfplay_channel_unit(const NSFCore* core, int32_t unit); // information about a unit +NSFChannelInfo nsfplay_channel_info(int32_t global_channel); // information about a global channel + +// an NSF has a list of active channels, and their playback state can be queried +int32_t nsfplay_channel_count(const NSFCore* core); // number of active channels +NSFChannelState nsfplay_channel_state(const NSFCore* core, int32_t active_channel); +// some channels have an extended state (e.g. FDS or N163 waveform) +// - returns number of bytes of extended state +// - copies up to data_size bytes to data if data is not NULL +uint32_t nsfplay_channel_state_ex(const NSFCore* core, int32_t active_channel, void* data, uint32_t data_size); + + +// other text strings adapted for the current locale, see: NSFP_TEXT_key +const char* nsfplay_local_text(int32_t key); -int test(); -}; +} // extern "C" diff --git a/include/nsfplayenums.h b/include/nsfplayenums.h new file mode 100644 index 0000000..558847b --- /dev/null +++ b/include/nsfplayenums.h @@ -0,0 +1,70 @@ +#pragma once +// generated by nsfplayenums.py +// Sun Apr 21 03:40:40 2024 + +#define NSFP_LIST_COUNT 2 +#define NSFP_LIST_ENABLE 0 +#define NSFP_LIST_LOCALE 1 + +#define NSFP_UNIT_COUNT 2 +#define NSFP_UNIT_APU0 0 +#define NSFP_UNIT_APU1 1 + +#define NSFP_GROUP_COUNT 3 +#define NSFP_GROUP_MAIN 0 +#define NSFP_GROUP_APU0 1 +#define NSFP_GROUP_APU1 2 + +#define NSFP_CHANNEL_COUNT 5 +#define NSFP_CHANNEL_APU0_SQU0 0 +#define NSFP_CHANNEL_APU0_SQU1 1 +#define NSFP_CHANNEL_APU1_TRI 2 +#define NSFP_CHANNEL_APU1_NSE 3 +#define NSFP_CHANNEL_APU1_DPCM 4 + +#define NSFP_SET_COUNT 20 +#define NSFP_SET_MAIN_VOLUME 0 +#define NSFP_SET_MAIN_SAMPLERATE 1 +#define NSFP_SET_MAIN_STEREO 2 +#define NSFP_SET_MAIN_TITLE_FORMAT 3 +#define NSFP_SET_MAIN_LOCALE 4 +#define NSFP_SET_APU0_SQU0_ON 5 +#define NSFP_SET_APU0_SQU0_VOL 6 +#define NSFP_SET_APU0_SQU0_PAN 7 +#define NSFP_SET_APU0_SQU1_ON 8 +#define NSFP_SET_APU0_SQU1_VOL 9 +#define NSFP_SET_APU0_SQU1_PAN 10 +#define NSFP_SET_APU1_TRI_ON 11 +#define NSFP_SET_APU1_TRI_VOL 12 +#define NSFP_SET_APU1_TRI_PAN 13 +#define NSFP_SET_APU1_NSE_ON 14 +#define NSFP_SET_APU1_NSE_VOL 15 +#define NSFP_SET_APU1_NSE_PAN 16 +#define NSFP_SET_APU1_DPCM_ON 17 +#define NSFP_SET_APU1_DPCM_VOL 18 +#define NSFP_SET_APU1_DPCM_PAN 19 + +#define NSFP_PROP_COUNT 5 +#define NSFP_PROP_SONGCOUNT 0 +#define NSFP_PROP_LONG 1 +#define NSFP_PROP_TITLE 2 +#define NSFP_PROP_INFO 3 +#define NSFP_PROP_BLOB 4 + +#define NSFP_SONGPROP_COUNT 6 +#define NSFP_SONGPROP_INT 0 +#define NSFP_SONGPROP_SONGTEST 1 +#define NSFP_SONGPROP_LONG 2 +#define NSFP_SONGPROP_TITLE 3 +#define NSFP_SONGPROP_INFO 4 +#define NSFP_SONGPROP_BLOB 5 + +#define NSFP_TEXT_COUNT 2 +#define NSFP_TEXT_TEXT 0 +#define NSFP_TEXT_ERROR_ERROR 1 + +#define NSFP_LOCALE_COUNT 2 +#define NSFP_LOCALE_ENGLISH 0 +#define NSFP_LOCALE_JAPANESE 1 + +// end of file diff --git a/include/nsfplaygui.h b/include/nsfplaygui.h index 24708e2..45769a3 100644 --- a/include/nsfplaygui.h +++ b/include/nsfplaygui.h @@ -1,9 +1,25 @@ -// stub +#pragma once -// this is the public interface to the gui library -namespace nsfplaygui +// NSFPlay gui library public interface + +// void* is used to pass/return a WXWidget handle + +#include "nsfplaycore.h" + +extern "C" { -int test(); +// opens an about window (modal) +void* nsfplay_gui_about(void * parent); + +// opens a control window for the core +// - returns a window handle as a void* (modeless) +// - core_lock/unlock will be used to mutex all acces to the core state, if not NULL +void* nsfplay_gui_core(NSFCore* core, void* parent, void (*core_lock)(), void (*core_unlock)()); + +// opens a settings window for the core +void* nsfplay_gui_settings(NSFCore* core, void* parent, bool modal, void (*core_lock)(), void (*core_unlock)()); + +// TODO other panels? channels? etc.? -}; +} // extern "C" diff --git a/makefile b/makefile index 8d70bff..cfe3aba 100644 --- a/makefile +++ b/makefile @@ -1,6 +1,6 @@ include makefile.common -.PHONY: default core cmd gui nsfplay mac winamp icons wxlib install uninstall clean +.PHONY: default core cmd gui nsfplay mac winamp icons enums enums_verify wxlib install uninstall clean default: core cmd nsfplay @@ -25,6 +25,12 @@ winamp: core gui icons: $(MAKE) -C icons +enums: + $(MAKE) -C enums + +enums_verify: + $(MAKE) -C enums verify + wxlib: $(MAKE) -f makefile.wx diff --git a/readme.md b/readme.md index c135b01..00698c7 100644 --- a/readme.md +++ b/readme.md @@ -23,6 +23,7 @@ Support: * `gui` - A library that provides a common user interface, allowing a plugin to share the same UI as the stand alone player. * `include` - Public interfaces for the `core` and `gui` libraries. * `icons` - Icons used for the GUI, edit the PNG files, and rebuild the ICO copies using the makefile (imagemagick required). +* `enum` - Definitions of all settings and text used by NSFPlay, allowing localized text in multiple languages. * `wx` - [wxWidgets v3.2.4](https://github.com/wxWidgets/wxWidgets/tree/v3.2.4) cross platform GUI library. ## Build @@ -88,6 +89,8 @@ On Windows (including MSYS2), you don't need to keep the cmake generated directo * `make install prefix=~/my/directory` - install to custom directory. * `make uninstall prefix=~/my/directory` - uninstall from custom directory. * `make icons` - rebuild windows icons from PNG source. +* `make enums` - generate enums and text data. +* `make enums_verify` - verify that current generated enums/text data match their source. * `make mac` - packages `nsfplay` into `nsfplay.app` (build nsfplay first). * `make wxlib` - fetches wxWidgets submodule in `wx/` and builds libraries to `wxlib/`.