diff --git a/README.md b/README.md index 8e402ad..fac13e4 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,13 @@ void test() Per default it listens on port 6789 on localhost and accepts "raw" TCP connections. `dmdserver` accepts these command line options: +* -c --config + * Config file + * optional + * default is no config file +* -o --alt-color-path + * "Fixed alt color path, overwriting paths transmitted by DMDUpdates + * optional * -a --addr * IP address or host name * optional @@ -153,6 +160,40 @@ That means that the data of the last client that connected get displayed. All pr As soon as the last connection gets terminated by the client, the newest previous one becomes active again (if it is still active). The "paused" connections aren't really paused. Their data is still accepted but dropped instead of dispalyed. +### Config File + +```ini +[DMDServer] +# The address (interface) to listen for TCP connections +Addr=localhost +# The port to listen for TCP connections +Port=6789 +# Set to 1 if Serum colorization should be used +AltColor= +# Overwrite the AltColorPath sent by the client and set it to a fixed value +AltColorPath= + +[ZeDMD] +# Set to 1 if ZeDMD is attached +Enabled= +# Disable auto-detection and provide a fixed serial port +Device= +# Enable ZeDMD debug mode +Debug= +# Overwrite ZeDMD internal RGB order setting +RGBOrder= +# Overwrite ZeDMD internal brightness setting +Brightness= +# Set to 1 to permantenly store the overwritten settings above in ZeDMD internally +SaveSettings= + +[Pixelcade] +# Set to 1 if Pixelcade is attached +Enabled= +# Disable auto-detection and provide a fixed serial port +Device= +``` + ## Building: #### Windows (x64) diff --git a/include/DMDUtil/Config.h b/include/DMDUtil/Config.h index 2e01058..4ed5564 100644 --- a/include/DMDUtil/Config.h +++ b/include/DMDUtil/Config.h @@ -22,6 +22,8 @@ class DMDUTILAPI Config static Config* GetInstance(); bool IsAltColor() const { return m_altColor; } void SetAltColor(bool altColor) { m_altColor = altColor; } + void SetAltColorPath(const char* path) { m_altColorPath = path; } + const char* GetAltColorPath() const { return m_altColorPath.c_str(); } void SetIgnoreUnknownFramesTimeout(int framesTimeout) { m_framesTimeout = framesTimeout; } void SetMaximumUnknownFramesToSkip(int framesToSkip) { m_framesToSkip = framesToSkip; } int GetIgnoreUnknownFramesTimeout() { return m_framesTimeout; } @@ -57,6 +59,7 @@ class DMDUTILAPI Config static Config* m_pInstance; bool m_altColor; + std::string m_altColorPath; int m_framesTimeout; int m_framesToSkip; bool m_zedmd; diff --git a/include/DMDUtil/DMD.h b/include/DMDUtil/DMD.h index 84d3380..cb7993b 100644 --- a/include/DMDUtil/DMD.h +++ b/include/DMDUtil/DMD.h @@ -10,7 +10,7 @@ #define DMDUTIL_FRAME_BUFFER_SIZE 16 #define DMDUTIL_MAX_NAME_SIZE 16 -#define DMDUTIL_MAX_ALTCOLORPATH_SIZE 256 +#define DMDUTIL_MAX_PATH_SIZE 256 #define DMDUTIL_MAX_TRANSITIONAL_FRAME_DURATION 25 #include @@ -110,7 +110,7 @@ class DMDUTILAPI DMD { char header[9] = "AltColor"; char name[DMDUTIL_MAX_NAME_SIZE] = {0}; - char path[DMDUTIL_MAX_ALTCOLORPATH_SIZE] = {0}; + char path[DMDUTIL_MAX_PATH_SIZE] = {0}; }; #pragma pack(pop) // Reset to default packing @@ -158,7 +158,7 @@ class DMDUTILAPI DMD uint8_t m_updateBufferQueuePosition = 0; char m_romName[DMDUTIL_MAX_NAME_SIZE] = {0}; - char m_altColorPath[DMDUTIL_MAX_ALTCOLORPATH_SIZE] = {0}; + char m_altColorPath[DMDUTIL_MAX_PATH_SIZE] = {0}; AlphaNumeric* m_pAlphaNumeric; Serum* m_pSerum; ZeDMD* m_pZeDMD; diff --git a/platforms/android/arm64-v8a/external.sh b/platforms/android/arm64-v8a/external.sh index e98bb7f..6af550d 100755 --- a/platforms/android/arm64-v8a/external.sh +++ b/platforms/android/arm64-v8a/external.sh @@ -2,10 +2,10 @@ set -e -LIBCARGS_SHA=5949a20a926e902931de4a32adaad9f19c76f251 +CARGS_SHA=5949a20a926e902931de4a32adaad9f19c76f251 LIBZEDMD_SHA=08e98a858eb6e1394b4844bec7dd27c7c0d9a845 LIBSERUM_SHA=b69d2b436bc93570a2e7e78d0946cd3c43f7aed5 -LIBSOCKPP_SHA=e6c4688a576d95f42dd7628cefe68092f6c5cd0f +SOCKPP_SHA=e6c4688a576d95f42dd7628cefe68092f6c5cd0f if [[ $(uname) == "Linux" ]]; then NUM_PROCS=$(nproc) @@ -16,10 +16,10 @@ else fi echo "Building libraries..." -echo " LIBCARGS_SHA: ${LIBCARGS_SHA}" +echo " CARGS_SHA: ${CARGS_SHA}" echo " LIBZEDMD_SHA: ${LIBZEDMD_SHA}" echo " LIBSERUM_SHA: ${LIBSERUM_SHA}" -echo " LIBSOCKPP_SHA: ${LIBSOCKPP_SHA}" +echo " SOCKPP_SHA: ${SOCKPP_SHA}" echo "" if [ -z "${BUILD_TYPE}" ]; then @@ -35,12 +35,12 @@ mkdir external cd external # -# libcargs +# build cargs and copy to external # -curl -sL https://github.com/likle/cargs/archive/${LIBCARGS_SHA}.zip -o cargs.zip +curl -sL https://github.com/likle/cargs/archive/${CARGS_SHA}.zip -o cargs.zip unzip cargs.zip -cd cargs-${LIBCARGS_SHA} +cd cargs-${CARGS_SHA} cp include/cargs.h ../../third-party/include/ cmake -DBUILD_SHARED_LIBS=ON \ -DCMAKE_SYSTEM_NAME=Android \ @@ -83,12 +83,12 @@ cp build/libserum.so ../../third-party/runtime-libs/android/arm64-v8a/ cd .. # -# build libsockpp and copy to external +# build sockpp and copy to external # -curl -sL https://github.com/fpagliughi/sockpp/archive/${LIBSOCKPP_SHA}.zip -o sockpp.zip +curl -sL https://github.com/fpagliughi/sockpp/archive/${SOCKPP_SHA}.zip -o sockpp.zip unzip sockpp.zip -cd sockpp-$LIBSOCKPP_SHA +cd sockpp-$SOCKPP_SHA patch -p1 < ../../platforms/android/arm64-v8a/sockpp/001.patch cp -r include/sockpp ../../third-party/include/ cmake -DSOCKPP_BUILD_SHARED=ON \ diff --git a/platforms/ios/arm64/external.sh b/platforms/ios/arm64/external.sh index c99cde4..3ecb4c4 100755 --- a/platforms/ios/arm64/external.sh +++ b/platforms/ios/arm64/external.sh @@ -2,18 +2,18 @@ set -e -LIBCARGS_SHA=5949a20a926e902931de4a32adaad9f19c76f251 +CARGS_SHA=5949a20a926e902931de4a32adaad9f19c76f251 LIBZEDMD_SHA=08e98a858eb6e1394b4844bec7dd27c7c0d9a845 LIBSERUM_SHA=b69d2b436bc93570a2e7e78d0946cd3c43f7aed5 -LIBSOCKPP_SHA=e6c4688a576d95f42dd7628cefe68092f6c5cd0f +SOCKPP_SHA=e6c4688a576d95f42dd7628cefe68092f6c5cd0f NUM_PROCS=$(sysctl -n hw.ncpu) echo "Building libraries..." -echo " LIBCARGS_SHA: ${LIBCARGS_SHA}" +echo " CARGS_SHA: ${CARGS_SHA}" echo " LIBZEDMD_SHA: ${LIBZEDMD_SHA}" echo " LIBSERUM_SHA: ${LIBSERUM_SHA}" -echo " LIBSOCKPP_SHA: ${LIBSOCKPP_SHA}" +echo " SOCKPP_SHA: ${SOCKPP_SHA}" echo "" if [ -z "${BUILD_TYPE}" ]; then @@ -29,12 +29,12 @@ mkdir external cd external # -# libcargs +# build cargs and copy to external # -curl -sL https://github.com/likle/cargs/archive/${LIBCARGS_SHA}.zip -o cargs.zip +curl -sL https://github.com/likle/cargs/archive/${CARGS_SHA}.zip -o cargs.zip unzip cargs.zip -cd cargs-${LIBCARGS_SHA} +cd cargs-${CARGS_SHA} cp include/cargs.h ../../third-party/include/ mkdir build cd build @@ -72,12 +72,12 @@ cp build/libserum.a ../../third-party/build-libs/ios/arm64/ cd .. # -# build libsockpp and copy to external +# build sockpp and copy to external # -curl -sL https://github.com/fpagliughi/sockpp/archive/${LIBSOCKPP_SHA}.zip -o sockpp.zip +curl -sL https://github.com/fpagliughi/sockpp/archive/${SOCKPP_SHA}.zip -o sockpp.zip unzip sockpp.zip -cd sockpp-$LIBSOCKPP_SHA +cd sockpp-$SOCKPP_SHA cp -r include/sockpp ../../third-party/include/ cmake -DSOCKPP_BUILD_SHARED=OFF \ -DSOCKPP_BUILD_STATIC=ON \ diff --git a/platforms/linux/aarch64/external.sh b/platforms/linux/aarch64/external.sh index c782b30..1975be9 100755 --- a/platforms/linux/aarch64/external.sh +++ b/platforms/linux/aarch64/external.sh @@ -2,18 +2,18 @@ set -e -LIBCARGS_SHA=5949a20a926e902931de4a32adaad9f19c76f251 +CARGS_SHA=5949a20a926e902931de4a32adaad9f19c76f251 LIBZEDMD_SHA=08e98a858eb6e1394b4844bec7dd27c7c0d9a845 LIBSERUM_SHA=b69d2b436bc93570a2e7e78d0946cd3c43f7aed5 -LIBSOCKPP_SHA=e6c4688a576d95f42dd7628cefe68092f6c5cd0f +SOCKPP_SHA=e6c4688a576d95f42dd7628cefe68092f6c5cd0f NUM_PROCS=$(nproc) echo "Building libraries..." -echo " LIBCARGS_SHA: ${LIBCARGS_SHA}" +echo " CARGS_SHA: ${CARGS_SHA}" echo " LIBZEDMD_SHA: ${LIBZEDMD_SHA}" echo " LIBSERUM_SHA: ${LIBSERUM_SHA}" -echo " LIBSOCKPP_SHA: ${LIBSOCKPP_SHA}" +echo " SOCKPP_SHA: ${SOCKPP_SHA}" echo "" if [ -z "${BUILD_TYPE}" ]; then @@ -29,18 +29,18 @@ mkdir external cd external # -# libcargs +# build cargs and copy to external # -curl -sL https://github.com/likle/cargs/archive/${LIBCARGS_SHA}.zip -o cargs.zip +curl -sL https://github.com/likle/cargs/archive/${CARGS_SHA}.zip -o cargs.zip unzip cargs.zip -cd cargs-${LIBCARGS_SHA} +cd cargs-${CARGS_SHA} cp include/cargs.h ../../third-party/include/ mkdir build cd build cmake -DBUILD_SHARED_LIBS=ON .. make -cp -P libcargs.so* ../../../third-party/runtime-libs/linux/aarch64/ +cp -a libcargs.so* ../../../third-party/runtime-libs/linux/aarch64/ cd ../.. # @@ -75,12 +75,12 @@ cp -a build/libserum.{so,so.*} ../../third-party/runtime-libs/linux/aarch64/ cd .. # -# build libsockpp and copy to external +# build sockpp and copy to external # -curl -sL https://github.com/fpagliughi/sockpp/archive/${LIBSOCKPP_SHA}.zip -o sockpp.zip +curl -sL https://github.com/fpagliughi/sockpp/archive/${SOCKPP_SHA}.zip -o sockpp.zip unzip sockpp.zip -cd sockpp-$LIBSOCKPP_SHA +cd sockpp-$SOCKPP_SHA cp -r include/sockpp ../../third-party/include/ cmake -DCMAKE_BUILD_TYPE=${BUILD_TYPE} -B build cmake --build build -- -j${NUM_PROCS} diff --git a/platforms/linux/x64/external.sh b/platforms/linux/x64/external.sh index ae478c1..b104f0b 100755 --- a/platforms/linux/x64/external.sh +++ b/platforms/linux/x64/external.sh @@ -2,18 +2,18 @@ set -e -LIBCARGS_SHA=5949a20a926e902931de4a32adaad9f19c76f251 +CARGS_SHA=5949a20a926e902931de4a32adaad9f19c76f251 LIBZEDMD_SHA=08e98a858eb6e1394b4844bec7dd27c7c0d9a845 LIBSERUM_SHA=b69d2b436bc93570a2e7e78d0946cd3c43f7aed5 -LIBSOCKPP_SHA=e6c4688a576d95f42dd7628cefe68092f6c5cd0f +SOCKPP_SHA=e6c4688a576d95f42dd7628cefe68092f6c5cd0f NUM_PROCS=$(nproc) echo "Building libraries..." -echo " LIBCARGS_SHA: ${LIBCARGS_SHA}" +echo " CARGS_SHA: ${CARGS_SHA}" echo " LIBZEDMD_SHA: ${LIBZEDMD_SHA}" echo " LIBSERUM_SHA: ${LIBSERUM_SHA}" -echo " LIBSOCKPP_SHA: ${LIBSOCKPP_SHA}" +echo " SOCKPP_SHA: ${SOCKPP_SHA}" echo "" if [ -z "${BUILD_TYPE}" ]; then @@ -29,18 +29,18 @@ mkdir external cd external # -# libcargs +# build cargs and copy to external # -curl -sL https://github.com/likle/cargs/archive/${LIBCARGS_SHA}.zip -o cargs.zip +curl -sL https://github.com/likle/cargs/archive/${CARGS_SHA}.zip -o cargs.zip unzip cargs.zip -cd cargs-${LIBCARGS_SHA} +cd cargs-${CARGS_SHA} cp include/cargs.h ../../third-party/include/ mkdir build cd build cmake -DBUILD_SHARED_LIBS=ON .. make -cp -P libcargs.so* ../../../third-party/runtime-libs/linux/x64/ +cp -a libcargs.so* ../../../third-party/runtime-libs/linux/x64/ cd ../.. # @@ -75,12 +75,12 @@ cp -a build/libserum.{so,so.*} ../../third-party/runtime-libs/linux/x64/ cd .. # -# build libsockpp and copy to external +# build sockpp and copy to external # -curl -sL https://github.com/fpagliughi/sockpp/archive/${LIBSOCKPP_SHA}.zip -o sockpp.zip +curl -sL https://github.com/fpagliughi/sockpp/archive/${SOCKPP_SHA}.zip -o sockpp.zip unzip sockpp.zip -cd sockpp-$LIBSOCKPP_SHA +cd sockpp-$SOCKPP_SHA cp -r include/sockpp ../../third-party/include/ cmake -DCMAKE_BUILD_TYPE=${BUILD_TYPE} -B build cmake --build build -- -j${NUM_PROCS} diff --git a/platforms/macos/arm64/external.sh b/platforms/macos/arm64/external.sh index 820c2f2..2ebe7b1 100755 --- a/platforms/macos/arm64/external.sh +++ b/platforms/macos/arm64/external.sh @@ -2,17 +2,18 @@ set -e -LIBCARGS_SHA=5949a20a926e902931de4a32adaad9f19c76f251 +CARGS_SHA=5949a20a926e902931de4a32adaad9f19c76f251 LIBZEDMD_SHA=08e98a858eb6e1394b4844bec7dd27c7c0d9a845 LIBSERUM_SHA=b69d2b436bc93570a2e7e78d0946cd3c43f7aed5 -LIBSOCKPP_SHA=e6c4688a576d95f42dd7628cefe68092f6c5cd0f +SOCKPP_SHA=e6c4688a576d95f42dd7628cefe68092f6c5cd0f NUM_PROCS=$(sysctl -n hw.ncpu) echo "Building libraries..." +echo " CARGS_SHA: ${CARGS_SHA}" echo " LIBZEDMD_SHA: ${LIBZEDMD_SHA}" echo " LIBSERUM_SHA: ${LIBSERUM_SHA}" -echo " LIBSOCKPP_SHA: ${LIBSOCKPP_SHA}" +echo " SOCKPP_SHA: ${SOCKPP_SHA}" echo "" if [ -z "${BUILD_TYPE}" ]; then @@ -28,18 +29,18 @@ mkdir external cd external # -# libcargs +# build cargs and copy to external # -curl -sL https://github.com/likle/cargs/archive/${LIBCARGS_SHA}.zip -o cargs.zip +curl -sL https://github.com/likle/cargs/archive/${CARGS_SHA}.zip -o cargs.zip unzip cargs.zip -cd cargs-${LIBCARGS_SHA} +cd cargs-${CARGS_SHA} cp include/cargs.h ../../third-party/include/ mkdir build cd build cmake -DCMAKE_OSX_ARCHITECTURES=arm64 -DBUILD_SHARED_LIBS=ON .. make -cp -P libcargs*.dylib ../../../third-party/runtime-libs/macos/arm64/ +cp -a libcargs*.dylib ../../../third-party/runtime-libs/macos/arm64/ cd ../.. # @@ -73,12 +74,12 @@ cp -a build/*.dylib ../../third-party/runtime-libs/macos/arm64/ cd .. # -# build libsockpp and copy to external +# build sockpp and copy to external # -curl -sL https://github.com/fpagliughi/sockpp/archive/${LIBSOCKPP_SHA}.zip -o sockpp.zip +curl -sL https://github.com/fpagliughi/sockpp/archive/${SOCKPP_SHA}.zip -o sockpp.zip unzip sockpp.zip -cd sockpp-$LIBSOCKPP_SHA +cd sockpp-$SOCKPP_SHA cp -r include/sockpp ../../third-party/include/ cmake -DSOCKPP_BUILD_SHARED=ON \ -DSOCKPP_BUILD_STATIC=OFF \ diff --git a/platforms/macos/x64/external.sh b/platforms/macos/x64/external.sh index ae7ce7c..a89f6eb 100755 --- a/platforms/macos/x64/external.sh +++ b/platforms/macos/x64/external.sh @@ -2,18 +2,18 @@ set -e -LIBCARGS_SHA=5949a20a926e902931de4a32adaad9f19c76f251 +CARGS_SHA=5949a20a926e902931de4a32adaad9f19c76f251 LIBZEDMD_SHA=08e98a858eb6e1394b4844bec7dd27c7c0d9a845 LIBSERUM_SHA=b69d2b436bc93570a2e7e78d0946cd3c43f7aed5 -LIBSOCKPP_SHA=e6c4688a576d95f42dd7628cefe68092f6c5cd0f +SOCKPP_SHA=e6c4688a576d95f42dd7628cefe68092f6c5cd0f NUM_PROCS=$(sysctl -n hw.ncpu) echo "Building libraries..." -echo " LIBCARGS_SHA: ${LIBCARGS_SHA}" +echo " CARGS_SHA: ${CARGS_SHA}" echo " LIBZEDMD_SHA: ${LIBZEDMD_SHA}" echo " LIBSERUM_SHA: ${LIBSERUM_SHA}" -echo " LIBSOCKPP_SHA: ${LIBSOCKPP_SHA}" +echo " SOCKPP_SHA: ${SOCKPP_SHA}" echo "" if [ -z "${BUILD_TYPE}" ]; then @@ -29,18 +29,18 @@ mkdir external cd external # -# libcargs +# build cargs and copy to external # -curl -sL https://github.com/likle/cargs/archive/${LIBCARGS_SHA}.zip -o cargs.zip +curl -sL https://github.com/likle/cargs/archive/${CARGS_SHA}.zip -o cargs.zip unzip cargs.zip -cd cargs-${LIBCARGS_SHA} +cd cargs-${CARGS_SHA} cp include/cargs.h ../../third-party/include/ mkdir build cd build cmake -DBUILD_SHARED_LIBS=ON .. make -cp -P libcargs*.dylib ../../../third-party/runtime-libs/macos/x64/ +cp -a libcargs*.dylib ../../../third-party/runtime-libs/macos/x64/ cd ../.. # @@ -74,12 +74,12 @@ cp -a build/*.dylib ../../third-party/runtime-libs/macos/x64/ cd .. # -# build libsockpp and copy to external +# build sockpp and copy to external # -curl -sL https://github.com/fpagliughi/sockpp/archive/${LIBSOCKPP_SHA}.zip -o sockpp.zip +curl -sL https://github.com/fpagliughi/sockpp/archive/${SOCKPP_SHA}.zip -o sockpp.zip unzip sockpp.zip -cd sockpp-$LIBSOCKPP_SHA +cd sockpp-$SOCKPP_SHA cp -r include/sockpp ../../third-party/include/ cmake -DSOCKPP_BUILD_SHARED=ON \ -DSOCKPP_BUILD_STATIC=OFF \ diff --git a/platforms/tvos/arm64/external.sh b/platforms/tvos/arm64/external.sh index fd86829..3ee1501 100755 --- a/platforms/tvos/arm64/external.sh +++ b/platforms/tvos/arm64/external.sh @@ -2,18 +2,18 @@ set -e -LIBCARGS_SHA=5949a20a926e902931de4a32adaad9f19c76f251 +CARGS_SHA=5949a20a926e902931de4a32adaad9f19c76f251 LIBZEDMD_SHA=08e98a858eb6e1394b4844bec7dd27c7c0d9a845 LIBSERUM_SHA=b69d2b436bc93570a2e7e78d0946cd3c43f7aed5 -LIBSOCKPP_SHA=e6c4688a576d95f42dd7628cefe68092f6c5cd0f +SOCKPP_SHA=e6c4688a576d95f42dd7628cefe68092f6c5cd0f NUM_PROCS=$(sysctl -n hw.ncpu) echo "Building libraries..." -echo " LIBCARGS_SHA: ${LIBCARGS_SHA}" +echo " CARGS_SHA: ${CARGS_SHA}" echo " LIBZEDMD_SHA: ${LIBZEDMD_SHA}" echo " LIBSERUM_SHA: ${LIBSERUM_SHA}" -echo " LIBSOCKPP_SHA: ${LIBSOCKPP_SHA}" +echo " SOCKPP_SHA: ${SOCKPP_SHA}" echo "" if [ -z "${BUILD_TYPE}" ]; then @@ -29,12 +29,12 @@ mkdir external cd external # -# libcargs +# build cargs and copy to external # -curl -sL https://github.com/likle/cargs/archive/${LIBCARGS_SHA}.zip -o cargs.zip +curl -sL https://github.com/likle/cargs/archive/${CARGS_SHA}.zip -o cargs.zip unzip cargs.zip -cd cargs-${LIBCARGS_SHA} +cd cargs-${CARGS_SHA} cp include/cargs.h ../../third-party/include/ mkdir build cd build @@ -72,12 +72,12 @@ cp build/libserum.a ../../third-party/build-libs/tvos/arm64/ cd .. # -# build libsockpp and copy to external +# build sockpp and copy to external # -curl -sL https://github.com/fpagliughi/sockpp/archive/${LIBSOCKPP_SHA}.zip -o sockpp.zip +curl -sL https://github.com/fpagliughi/sockpp/archive/${SOCKPP_SHA}.zip -o sockpp.zip unzip sockpp.zip -cd sockpp-$LIBSOCKPP_SHA +cd sockpp-$SOCKPP_SHA cp -r include/sockpp ../../third-party/include/ cmake -DSOCKPP_BUILD_SHARED=OFF \ -DSOCKPP_BUILD_STATIC=ON \ diff --git a/platforms/win/x64/external.sh b/platforms/win/x64/external.sh index 6f06f18..177d7eb 100755 --- a/platforms/win/x64/external.sh +++ b/platforms/win/x64/external.sh @@ -2,16 +2,16 @@ set -e -LIBCARGS_SHA=5949a20a926e902931de4a32adaad9f19c76f251 +CARGS_SHA=5949a20a926e902931de4a32adaad9f19c76f251 LIBZEDMD_SHA=08e98a858eb6e1394b4844bec7dd27c7c0d9a845 LIBSERUM_SHA=b69d2b436bc93570a2e7e78d0946cd3c43f7aed5 -LIBSOCKPP_SHA=e6c4688a576d95f42dd7628cefe68092f6c5cd0f +SOCKPP_SHA=e6c4688a576d95f42dd7628cefe68092f6c5cd0f echo "Building libraries..." -echo " LIBCARGS_SHA: ${LIBCARGS_SHA}" +echo " CARGS_SHA: ${CARGS_SHA}" echo " LIBZEDMD_SHA: ${LIBZEDMD_SHA}" echo " LIBSERUM_SHA: ${LIBSERUM_SHA}" -echo " LIBSOCKPP_SHA: ${LIBSOCKPP_SHA}" +echo " SOCKPP_SHA: ${SOCKPP_SHA}" echo "" if [ -z "${BUILD_TYPE}" ]; then @@ -26,12 +26,12 @@ mkdir external cd external # -# libcargs +# build cargs and copy to external # -curl -sL https://github.com/likle/cargs/archive/${LIBCARGS_SHA}.zip -o cargs.zip +curl -sL https://github.com/likle/cargs/archive/${CARGS_SHA}.zip -o cargs.zip unzip cargs.zip -cd cargs-${LIBCARGS_SHA} +cd cargs-${CARGS_SHA} patch -p1 < ../../platforms/win/x64/cargs/001.patch cp include/cargs.h ../../third-party/include/ cmake -G "Visual Studio 17 2022" -DBUILD_SHARED_LIBS=ON -B build @@ -74,12 +74,12 @@ cp build/${BUILD_TYPE}/serum64.dll ../../third-party/runtime-libs/win/x64/ cd .. # -# build libsockpp and copy to external +# build sockpp and copy to external # -curl -sL https://github.com/fpagliughi/sockpp/archive/${LIBSOCKPP_SHA}.zip -o sockpp.zip +curl -sL https://github.com/fpagliughi/sockpp/archive/${SOCKPP_SHA}.zip -o sockpp.zip unzip sockpp.zip -cd sockpp-$LIBSOCKPP_SHA +cd sockpp-$SOCKPP_SHA patch -p1 < ../../platforms/win/x64/sockpp/001.patch cp -r include/sockpp ../../third-party/include/ cmake -G "Visual Studio 17 2022" -B build diff --git a/platforms/win/x86/external.sh b/platforms/win/x86/external.sh index 1292ed4..5220229 100755 --- a/platforms/win/x86/external.sh +++ b/platforms/win/x86/external.sh @@ -2,16 +2,16 @@ set -e -LIBCARGS_SHA=5949a20a926e902931de4a32adaad9f19c76f251 +CARGS_SHA=5949a20a926e902931de4a32adaad9f19c76f251 LIBZEDMD_SHA=08e98a858eb6e1394b4844bec7dd27c7c0d9a845 LIBSERUM_SHA=b69d2b436bc93570a2e7e78d0946cd3c43f7aed5 -LIBSOCKPP_SHA=e6c4688a576d95f42dd7628cefe68092f6c5cd0f +SOCKPP_SHA=e6c4688a576d95f42dd7628cefe68092f6c5cd0f echo "Building libraries..." -echo " LIBCARGS_SHA: ${LIBCARGS_SHA}" +echo " CARGS_SHA: ${CARGS_SHA}" echo " LIBZEDMD_SHA: ${LIBZEDMD_SHA}" echo " LIBSERUM_SHA: ${LIBSERUM_SHA}" -echo " LIBSOCKPP_SHA: ${LIBSOCKPP_SHA}" +echo " SOCKPP_SHA: ${SOCKPP_SHA}" echo "" if [ -z "${BUILD_TYPE}" ]; then @@ -26,12 +26,12 @@ mkdir external cd external # -# libcargs +# build cargs and copy to external # -curl -sL https://github.com/likle/cargs/archive/${LIBCARGS_SHA}.zip -o cargs.zip +curl -sL https://github.com/likle/cargs/archive/${CARGS_SHA}.zip -o cargs.zip unzip cargs.zip -cd cargs-${LIBCARGS_SHA} +cd cargs-${CARGS_SHA} cp include/cargs.h ../../third-party/include/ cmake -G "Visual Studio 17 2022" -DBUILD_SHARED_LIBS=ON -A Win32 -B build cmake --build build --config ${BUILD_TYPE} @@ -73,12 +73,12 @@ cp build/${BUILD_TYPE}/serum.dll ../../third-party/runtime-libs/win/x86/ cd .. # -# build libsockpp and copy to external +# build sockpp and copy to external # -curl -sL https://github.com/fpagliughi/sockpp/archive/${LIBSOCKPP_SHA}.zip -o sockpp.zip +curl -sL https://github.com/fpagliughi/sockpp/archive/${SOCKPP_SHA}.zip -o sockpp.zip unzip sockpp.zip -cd sockpp-$LIBSOCKPP_SHA +cd sockpp-$SOCKPP_SHA cp -r include/sockpp ../../third-party/include/ cmake -G "Visual Studio 17 2022" -A Win32 -B build cmake --build build --config ${BUILD_TYPE} diff --git a/src/Config.cpp b/src/Config.cpp index 2c5cc9d..0d5ab15 100644 --- a/src/Config.cpp +++ b/src/Config.cpp @@ -15,6 +15,7 @@ Config* Config::GetInstance() Config::Config() { m_altColor = true; + m_altColorPath.clear(); m_framesTimeout = 0; m_framesToSkip = 0; m_zedmd = true; diff --git a/src/DMD.cpp b/src/DMD.cpp index 8bd1d7e..ac07540 100644 --- a/src/DMD.cpp +++ b/src/DMD.cpp @@ -494,6 +494,8 @@ void DMD::DmdFrameThread() strcpy(name, m_romName); + if (m_altColorPath[0] == '\0') strcpy(m_altColorPath, Config::GetInstance()->GetAltColorPath()); + m_pSerum = (Config::GetInstance()->IsAltColor() && name[0] != '\0') ? Serum::Load(m_altColorPath, m_romName) : nullptr; if (m_pSerum) diff --git a/src/dmdServer.cpp b/src/dmdServer.cpp index f682458..5bcf96f 100644 --- a/src/dmdServer.cpp +++ b/src/dmdServer.cpp @@ -8,6 +8,7 @@ #include "DMDUtil/DMDUtil.h" #include "Logger.h" #include "cargs.h" +#include "ini.h" #include "sockpp/tcp_acceptor.h" #define DMDSERVER_MAX_WIDTH 256 @@ -19,8 +20,19 @@ DMDUtil::DMD* pDmd; uint32_t currentThreadId = 0; std::vector threads; bool opt_verbose = false; +bool opt_fixedAltColorPath = false; static struct cag_option options[] = { + {.identifier = 'c', + .access_letters = "c", + .access_name = "config", + .value_name = "VALUE", + .description = "Config file (optional, default is no config file)"}, + {.identifier = 'o', + .access_letters = "o", + .access_name = "alt-color-path", + .value_name = "VALUE", + .description = "Fixed alt color path, overwriting paths transmitted by DMDUpdates (optional)"}, {.identifier = 'a', .access_letters = "a", .access_name = "addr", @@ -101,7 +113,7 @@ void run(sockpp::tcp_socket sock, uint32_t threadId) if (data.width <= DMDSERVER_MAX_WIDTH && data.height <= DMDSERVER_MAX_HEIGHT) { pDmd->SetRomName(altColorHeader.name); - pDmd->SetAltColorPath(altColorHeader.path); + if (!opt_fixedAltColorPath) pDmd->SetAltColorPath(altColorHeader.path); pDmd->QueueUpdate(data, pStreamHeader->buffered == 1); } @@ -182,45 +194,63 @@ int main(int argc, char* argv[]) cag_option_context cag_context; bool opt_wait = false; - const char* pPort = nullptr; cag_option_prepare(&cag_context, options, CAG_ARRAY_SIZE(options), argc, argv); while (cag_option_fetch(&cag_context)) { char identifier = cag_option_get(&cag_context); - switch (identifier) + if (identifier == 'c') { - case 'a': - pConfig->SetDMDServerAddr(cag_option_get_value(&cag_context)); - break; - - case 'p': - pPort = cag_option_get_value(&cag_context); - break; - - case 'w': - opt_wait = true; - break; - - case 'v': - opt_verbose = true; - case 'l': - pConfig->SetLogCallback(LogCallback); - break; - - case 'h': - cout << "Usage: dmdserver [OPTION]..." << endl; - cag_option_print(options, CAG_ARRAY_SIZE(options), stdout); - return 0; + inih::INIReader r{cag_option_get_value(&cag_context)}; + pConfig->SetDMDServerAddr(r.Get("DMDServer", "Addr").c_str()); + pConfig->SetDMDServerPort(r.Get("DMDServer", "Port")); + pConfig->SetAltColor(r.Get("DMDServer", "AltColor")); + pConfig->SetAltColorPath(r.Get("DMDServer", "AltColorPath").c_str()); + // ZeDMD + pConfig->SetZeDMD(r.Get("ZeDMD", "Enabled")); + pConfig->SetZeDMDDevice(r.Get("ZeDMD", "Device").c_str()); + pConfig->SetZeDMDDebug(r.Get("ZeDMD", "Debug")); + pConfig->SetZeDMDRGBOrder(r.Get("ZeDMD", "RGBOrder")); + pConfig->SetZeDMDBrightness(r.Get("ZeDMD", "Brightness")); + pConfig->SetZeDMDSaveSettings(r.Get("DMDServer", "SaveSettings")); + // Pixelcade + pConfig->SetPixelcade(r.Get("Pixelcade", "Enabled")); + pConfig->SetPixelcadeDevice(r.Get("Pixelcade", "Device").c_str()); + } + else if (identifier == 'o') + { + pConfig->SetAltColorPath(cag_option_get_value(&cag_context)); + } + else if (identifier == 'a') + { + pConfig->SetDMDServerAddr(cag_option_get_value(&cag_context)); + } + else if (identifier == 'p') + { + std::stringstream ssPort(cag_option_get_value(&cag_context)); + in_port_t port; + ssPort >> port; + pConfig->SetDMDServerPort(port); + } + else if (identifier == 'w') + { + opt_wait = true; + } + else if (identifier == 'v') + { + opt_verbose = true; + pConfig->SetLogCallback(LogCallback); + } + else if (identifier == 'l') + { + pConfig->SetLogCallback(LogCallback); + } + else if (identifier == 'h') + { + cout << "Usage: dmdserver [OPTION]..." << endl; + cag_option_print(options, CAG_ARRAY_SIZE(options), stdout); + return 0; } - } - - if (pPort) - { - in_port_t port; - std::stringstream ssPort(pPort); - ssPort >> port; - pConfig->SetDMDServerPort(port); } sockpp::initialize(); @@ -246,6 +276,13 @@ int main(int argc, char* argv[]) this_thread::sleep_for(chrono::milliseconds(1000)); } + std::string altColorPath = DMDUtil::Config::GetInstance()->GetAltColorPath(); + if (!altColorPath.empty()) + { + pDmd->SetAltColorPath(altColorPath.c_str()); + opt_fixedAltColorPath = true; + } + while (pDmd->HasDisplay()) { sockpp::inet_address peer; diff --git a/third-party/README.md b/third-party/README.md new file mode 100644 index 0000000..2f17db4 --- /dev/null +++ b/third-party/README.md @@ -0,0 +1,28 @@ +# Third party libraries + + +## ini-cpp + +- Upstream: https://github.com/SSARCandy/ini-cpp +- License: New BSD license + +## cargs + +- Upstream: https://github.com/likle/cargs +- License: MIT License + +## sockpp + +- Upstream: https://github.com/fpagliughi/sockpp +- License: BSD 3-Clause License + +## libzedmd + +- Upstream: https://github.com/PPUC/libzedmd +- License: GPL-3.0 license + +## libserum + +- Upstream: https://github.com/zesinger/libserum +- License: GPL-2.0 license + diff --git a/third-party/include/ini.h b/third-party/include/ini.h new file mode 100644 index 0000000..00c9879 --- /dev/null +++ b/third-party/include/ini.h @@ -0,0 +1,624 @@ +/** + * Yet another .ini parser for modern c++ (made for cpp17), inspired and extend + * from @benhoyt's inih. See project page: https://github.com/SSARCandy/ini-cpp + */ + +#ifndef __INI_H__ +#define __INI_H__ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace inih { + +/* Typedef for prototype of handler function. */ +typedef int (*ini_handler)(void* user, const char* section, const char* name, + const char* value); + +/* Typedef for prototype of fgets-style reader function. */ +typedef char* (*ini_reader)(char* str, int num, void* stream); + +#define INI_STOP_ON_FIRST_ERROR 1 +#define INI_MAX_LINE 2000 +#define INI_INITIAL_ALLOC 200 +#define MAX_SECTION 50 +#define MAX_NAME 50 +#define INI_START_COMMENT_PREFIXES ";#" +#define INI_INLINE_COMMENT_PREFIXES ";" + +/* Strip whitespace chars off end of given string, in place. Return s. */ +inline static char* rstrip(char* s) { + char* p = s + strlen(s); + while (p > s && isspace((unsigned char)(*--p))) *p = '\0'; + return s; +} + +/* Return pointer to first non-whitespace char in given string. */ +inline static char* lskip(const char* s) { + while (*s && isspace((unsigned char)(*s))) s++; + return (char*)s; +} + +/* Return pointer to first char (of chars) or inline comment in given string, + or pointer to null at end of string if neither found. Inline comment must + be prefixed by a whitespace character to register as a comment. */ +inline static char* find_chars_or_comment(const char* s, const char* chars) { + int was_space = 0; + while (*s && (!chars || !strchr(chars, *s)) && + !(was_space && strchr(INI_INLINE_COMMENT_PREFIXES, *s))) { + was_space = isspace((unsigned char)(*s)); + s++; + } + return (char*)s; +} + +/* Version of strncpy that ensures dest (size bytes) is null-terminated. */ +inline static char* strncpy0(char* dest, const char* src, size_t size) { + strncpy(dest, src, size - 1); + dest[size - 1] = '\0'; + return dest; +} + +/* See documentation in header file. */ +inline int ini_parse_stream(ini_reader reader, void* stream, + ini_handler handler, void* user) { + /* Uses a fair bit of stack (use heap instead if you need to) */ + char* line; + size_t max_line = INI_INITIAL_ALLOC; + char* new_line; + size_t offset; + char section[MAX_SECTION] = ""; + char prev_name[MAX_NAME] = ""; + + char* start; + char* end; + char* name; + char* value; + int lineno = 0; + int error = 0; + + line = (char*)malloc(INI_INITIAL_ALLOC); + if (!line) { + return -2; + } + +#if INI_HANDLER_LINENO +#define HANDLER(u, s, n, v) handler(u, s, n, v, lineno) +#else +#define HANDLER(u, s, n, v) handler(u, s, n, v) +#endif + + /* Scan through stream line by line */ + while (reader(line, (int)max_line, stream) != NULL) { + offset = strlen(line); + while (offset == max_line - 1 && line[offset - 1] != '\n') { + max_line *= 2; + if (max_line > INI_MAX_LINE) max_line = INI_MAX_LINE; + new_line = (char*)realloc(line, max_line); + if (!new_line) { + free(line); + return -2; + } + line = new_line; + if (reader(line + offset, (int)(max_line - offset), stream) == NULL) + break; + if (max_line >= INI_MAX_LINE) break; + offset += strlen(line + offset); + } + + lineno++; + + start = line; + if (lineno == 1 && (unsigned char)start[0] == 0xEF && + (unsigned char)start[1] == 0xBB && + (unsigned char)start[2] == 0xBF) { + start += 3; + } + start = lskip(rstrip(start)); + + if (strchr(INI_START_COMMENT_PREFIXES, *start)) { + /* Start-of-line comment */ + } else if (*start == '[') { + /* A "[section]" line */ + end = find_chars_or_comment(start + 1, "]"); + if (*end == ']') { + *end = '\0'; + strncpy0(section, start + 1, sizeof(section)); + *prev_name = '\0'; + } else if (!error) { + /* No ']' found on section line */ + error = lineno; + } + } else if (*start) { + /* Not a comment, must be a name[=:]value pair */ + end = find_chars_or_comment(start, "=:"); + if (*end == '=' || *end == ':') { + *end = '\0'; + name = rstrip(start); + value = end + 1; + end = find_chars_or_comment(value, NULL); + if (*end) *end = '\0'; + value = lskip(value); + rstrip(value); + + /* Valid name[=:]value pair found, call handler */ + strncpy0(prev_name, name, sizeof(prev_name)); + if (!HANDLER(user, section, name, value) && !error) + error = lineno; + } else if (!error) { + /* No '=' or ':' found on name[=:]value line */ + error = lineno; + } + } + + if (error) break; + } + + free(line); + + return error; +} + +inline int ini_parse_file(FILE* file, ini_handler handler, void* user) { + return ini_parse_stream((ini_reader)fgets, file, handler, user); +} + +inline int ini_parse(const char* filename, ini_handler handler, void* user) { + FILE* file; + int error; + + file = fopen(filename, "r"); + if (!file) return -1; + error = ini_parse_file(file, handler, user); + fclose(file); + return error; +} + +#endif /* __INI_H__ */ + +#ifndef __INIREADER_H__ +#define __INIREADER_H__ + +// Read an INI file into easy-to-access name/value pairs. (Note that I've gone +// for simplicity here rather than speed, but it should be pretty decent.) +class INIReader { + public: + // Empty Constructor + INIReader(){}; + + // Construct INIReader and parse given filename. See ini.h for more info + // about the parsing. + INIReader(std::string filename); + + // Construct INIReader and parse given file. See ini.h for more info + // about the parsing. + INIReader(FILE* file); + + // Return the result of ini_parse(), i.e., 0 on success, line number of + // first error on parse error, or -1 on file open error. + int ParseError() const; + + // Return the list of sections found in ini file + const std::set Sections() const; + + // Return the list of keys in the given section + const std::set Keys(std::string section) const; + + const std::unordered_map Get( + std::string section) const; + + template + T Get(const std::string& section, const std::string& name) const; + + template + T Get(const std::string& section, const std::string& name, + T&& default_v) const; + + template + std::vector GetVector(const std::string& section, + const std::string& name) const; + + template + std::vector GetVector(const std::string& section, + const std::string& name, + const std::vector& default_v) const; + + template + void InsertEntry(const std::string& section, const std::string& name, + const T& v); + + template + void InsertEntry(const std::string& section, const std::string& name, + const std::vector& vs); + + template + void UpdateEntry(const std::string& section, const std::string& name, + const T& v); + + template + void UpdateEntry(const std::string& section, const std::string& name, + const std::vector& vs); + + protected: + int _error; + std::unordered_map> + _values; + static int ValueHandler(void* user, const char* section, const char* name, + const char* value); + + template + T Converter(const std::string& s) const; + + const bool BoolConverter(std::string s) const; + + template + std::string V2String(const T& v) const; + + template + std::string Vec2String(const std::vector& v) const; +}; + +#endif // __INIREADER_H__ + +#ifndef __INIREADER__ +#define __INIREADER__ + +/** + * @brief Construct an INIReader object from a file name + * @param filename The name of the INI file to parse + * @throws std::runtime_error if there is an error parsing the INI file + */ +inline INIReader::INIReader(std::string filename) { + _error = ini_parse(filename.c_str(), ValueHandler, this); + ParseError(); +} + +/** + * @brief Construct an INIReader object from a file pointer + * @param file A pointer to the INI file to parse + * @throws std::runtime_error if there is an error parsing the INI file + */ +inline INIReader::INIReader(FILE* file) { + _error = ini_parse_file(file, ValueHandler, this); + ParseError(); +} + +inline int INIReader::ParseError() const { + switch (_error) { + case 0: + break; + case -1: + throw std::runtime_error("ini file not found."); + case -2: + throw std::runtime_error("memory alloc error"); + default: + throw std::runtime_error("parse error on line no: " + + std::to_string(_error)); + } + return 0; +} + +/** + * @brief Return the list of sections found in ini file + * @return The list of sections found in ini file + */ +inline const std::set INIReader::Sections() const { + std::set retval; + for (auto const& element : _values) { + retval.insert(element.first); + } + return retval; +} + +/** + * @brief Return the list of keys in the given section + * @param section The section name + * @return The list of keys in the given section + */ +inline const std::set INIReader::Keys(std::string section) const { + auto const _section = Get(section); + std::set retval; + for (auto const& element : _section) { + retval.insert(element.first); + } + return retval; +} + +/** + * @brief Get the map representing the values in a section of the INI file + * @param section The name of the section to retrieve + * @return The map representing the values in the given section + * @throws std::runtime_error if the section is not found + */ +inline const std::unordered_map INIReader::Get( + std::string section) const { + auto const _section = _values.find(section); + if (_section == _values.end()) { + throw std::runtime_error("section '" + section + "' not found."); + } + return _section->second; +} + +/** + * @brief Return the value of the given key in the given section + * @param section The section name + * @param name The key name + * @return The value of the given key in the given section + */ +template +inline T INIReader::Get(const std::string& section, + const std::string& name) const { + auto const _section = Get(section); + auto const _value = _section.find(name); + + if (_value == _section.end()) { + throw std::runtime_error("key '" + name + "' not found in section '" + + section + "'."); + } + + std::string value = _value->second; + + if constexpr (std::is_same()) { + return value; + } else if constexpr (std::is_same()) { + return BoolConverter(value); + } else { + return Converter(value); + }; +} + +/** + * @brief Return the value of the given key in the given section, return default + * if not found + * @param section The section name + * @param name The key name + * @param default_v The default value + * @return The value of the given key in the given section, return default if + * not found + */ +template +inline T INIReader::Get(const std::string& section, const std::string& name, + T&& default_v) const { + try { + return Get(section, name); + } catch (std::runtime_error& e) { + return default_v; + } +} + +/** + * @brief Return the value array of the given key in the given section. + * @param section The section name + * @param name The key name + * @return The value array of the given key in the given section. + * + * For example: + * ```ini + * [section] + * key = 1 2 3 4 + * ``` + * ```cpp + * const auto vs = ini.GetVector>("section", "key"); + * // vs = {1, 2, 3, 4} + * ``` + */ +template +inline std::vector INIReader::GetVector(const std::string& section, + const std::string& name) const { + std::string value = Get(section, name); + + std::istringstream out{value}; + const std::vector strs{std::istream_iterator{out}, + std::istream_iterator()}; + try { + std::vector vs{}; + for (const std::string& s : strs) { + vs.emplace_back(Converter(s)); + } + return vs; + } catch (std::exception& e) { + throw std::runtime_error("cannot parse value " + value + + " to vector."); + } +} + +/** + * @brief Return the value array of the given key in the given section, return + * default if not found + * @param section The section name + * @param name The key name + * @param default_v The default value + * @return The value array of the given key in the given section, return default + * if not found + * + * @see INIReader::GetVector + */ +template +inline std::vector INIReader::GetVector( + const std::string& section, const std::string& name, + const std::vector& default_v) const { + try { + return GetVector(section, name); + } catch (std::runtime_error& e) { + return default_v; + }; +} + +/** + * @brief Insert a key-value pair into the INI file + * @param section The section name + * @param name The key name + * @param v The value to insert + * @throws std::runtime_error if the key already exists in the section + */ +template +inline void INIReader::InsertEntry(const std::string& section, + const std::string& name, const T& v) { + if (_values[section][name].size() > 0) { + throw std::runtime_error("duplicate key '" + std::string(name) + + "' in section '" + section + "'."); + } + _values[section][name] = V2String(v); +} + +/** + * @brief Insert a vector of values into the INI file + * @param section The section name + * @param name The key name + * @param vs The vector of values to insert + * @throws std::runtime_error if the key already exists in the section + */ +template +inline void INIReader::InsertEntry(const std::string& section, + const std::string& name, + const std::vector& vs) { + if (_values[section][name].size() > 0) { + throw std::runtime_error("duplicate key '" + std::string(name) + + "' in section '" + section + "'."); + } + _values[section][name] = Vec2String(vs); +} + +/** + * @brief Update a key-value pair in the INI file + * @param section The section name + * @param name The key name + * @param v The new value to set + * @throws std::runtime_error if the key does not exist in the section + */ +template +inline void INIReader::UpdateEntry(const std::string& section, + const std::string& name, const T& v) { + if (!_values[section][name].size()) { + throw std::runtime_error("key '" + std::string(name) + + "' not exist in section '" + section + "'."); + } + _values[section][name] = V2String(v); +} + +/** + * @brief Update a vector of values in the INI file + * @param section The section name + * @param name The key name + * @param vs The new vector of values to set + * @throws std::runtime_error if the key does not exist in the section + */ +template +inline void INIReader::UpdateEntry(const std::string& section, + const std::string& name, + const std::vector& vs) { + if (!_values[section][name].size()) { + throw std::runtime_error("key '" + std::string(name) + + "' not exist in section '" + section + "'."); + } + _values[section][name] = Vec2String(vs); +} + +template +inline std::string INIReader::V2String(const T& v) const { + std::stringstream ss; + ss << v; + return ss.str(); +} + +template +inline std::string INIReader::Vec2String(const std::vector& v) const { + if (v.empty()) { + return ""; + } + std::ostringstream oss; + std::copy(v.begin(), v.end() - 1, std::ostream_iterator(oss, " ")); + oss << v.back(); + + return oss.str(); +} + +template +inline T INIReader::Converter(const std::string& s) const { + try { + T v{}; + std::istringstream _{s}; + _.exceptions(std::ios::failbit); + _ >> v; + return v; + } catch (std::exception& e) { + throw std::runtime_error("cannot parse value '" + s + "' to type."); + }; +} + +inline const bool INIReader::BoolConverter(std::string s) const { + std::transform(s.begin(), s.end(), s.begin(), ::tolower); + static const std::unordered_map s2b{ + {"1", true}, {"true", true}, {"yes", true}, {"on", true}, + {"0", false}, {"false", false}, {"no", false}, {"off", false}, + }; + auto const value = s2b.find(s); + if (value == s2b.end()) { + throw std::runtime_error("'" + s + "' is not a valid boolean value."); + } + return value->second; +} + +inline int INIReader::ValueHandler(void* user, const char* section, + const char* name, const char* value) { + INIReader* reader = (INIReader*)user; + if (reader->_values[section][name].size() > 0) { + throw std::runtime_error("duplicate key '" + std::string(name) + + "' in section '" + section + "'."); + } + reader->_values[section][name] = value; + return 1; +} +#endif // __INIREADER__ + +#ifndef __INIWRITER_H__ +#define __INIWRITER_H__ + +class INIWriter { + public: + INIWriter(){}; + /** + * @brief Write the contents of an INI file to a new file + * @param filepath The path of the output file + * @param reader The INIReader object to write to the file + * @throws std::runtime_error if the output file already exists or cannot be + * opened + */ + inline static void write(const std::string& filepath, + const INIReader& reader) { + if (struct stat buf; stat(filepath.c_str(), &buf) == 0) { + throw std::runtime_error("file: " + filepath + " already exist."); + } + std::ofstream out; + out.open(filepath); + if (!out.is_open()) { + throw std::runtime_error("cannot open output file: " + filepath); + } + for (const auto& section : reader.Sections()) { + out << "[" << section << "]\n"; + for (const auto& key : reader.Keys(section)) { + out << key << "=" << reader.Get(section, key) << "\n"; + } + } + out.close(); + } +}; +} +#endif /* __INIWRITER_H__ */ \ No newline at end of file