diff --git a/include/DOF/DOF.h b/include/DOF/DOF.h index e5822d7..e7ec981 100644 --- a/include/DOF/DOF.h +++ b/include/DOF/DOF.h @@ -2,7 +2,7 @@ #define LIBDOF_VERSION_MAJOR 0 // X Digits #define LIBDOF_VERSION_MINOR 4 // Max 2 Digits -#define LIBDOF_VERSION_PATCH 0 // Max 2 Digits +#define LIBDOF_VERSION_PATCH 2 // Max 2 Digits #define _LIBDOF_STR(x) #x #define LIBDOF_STR(x) _LIBDOF_STR(x) diff --git a/src/Log.cpp b/src/Log.cpp index 8bc2c29..693c0c9 100644 --- a/src/Log.cpp +++ b/src/Log.cpp @@ -1,27 +1,225 @@ #include "Log.h" - #include "Logger.h" +#include "general/StringExtensions.h" +#include "../include/DOF/DOF.h" + +#include +#include +#include +#include +#include +#include namespace DOF { -std::string Log::m_filename = ""; -std::unordered_set Log::m_onceKeys = {}; +std::ofstream Log::m_logger; +bool Log::m_isInitialized = false; +bool Log::m_isOk = false; +bool Log::m_isEnabled = true; +std::mutex Log::m_locker; -void Log::Init() { } -void Log::AfterInit() { } -void Log::Finish() { } -void Log::WriteRaw(const char* format, ...) { } +std::string Log::m_filename = "./DirectOutput.log"; +std::string Log::m_instrumentations = ""; +std::unordered_set Log::m_activeInstrumentations; +std::vector Log::m_preLogFileLog; +std::unordered_set Log::m_onceKeys; -void Log::Write(const std::string& message) { ::DOF::Log(DOF_LogLevel_INFO, "%s", message.c_str()); } +void Log::SetInstrumentations(const std::string& instrumentations) +{ + m_instrumentations = instrumentations; + if (!instrumentations.empty()) + { + m_activeInstrumentations.clear(); + std::stringstream ss(instrumentations); + std::string item; + while (std::getline(ss, item, ',')) + { + item.erase(0, item.find_first_not_of(" \t")); + item.erase(item.find_last_not_of(" \t") + 1); + if (!item.empty()) + m_activeInstrumentations.insert(item); + } + } +} + +void Log::Init(bool enableLogging) +{ + std::lock_guard lock(m_locker); + m_isEnabled = enableLogging; + if (!m_isInitialized && m_isEnabled) + { + try + { + std::filesystem::path logPath(m_filename); + if (logPath.has_parent_path()) + { + std::filesystem::create_directories(logPath.parent_path()); + } -void Log::Warning(const std::string& message) { ::DOF::Log(DOF_LogLevel_WARN, "%s", message.c_str()); } + m_logger.open(m_filename, std::ios::app); + if (!m_logger.is_open()) + { + m_isOk = false; + m_isInitialized = true; + return; + } -void Log::Error(const std::string& message) { ::DOF::Log(DOF_LogLevel_ERROR, "%s", message.c_str()); } + std::string buildConfiguration = +#ifdef _DEBUG + "Debug"; +#else + "Release"; +#endif -void Log::Exception(const std::string& message) { ::DOF::Log(DOF_LogLevel_ERROR, "%s", message.c_str()); } + std::string instrumentationsEnabledNote = (!m_activeInstrumentations.empty()) ? StringExtensions::Build("; Instrumentations enabled: {0}", m_instrumentations) : ""; -void Log::Debug(const std::string& message) { ::DOF::Log(DOF_LogLevel_DEBUG, "%s", message.c_str()); } + m_logger << "---------------------------------------------------------------------------------" << std::endl; + auto now = std::chrono::system_clock::now(); + auto time_t = std::chrono::system_clock::to_time_t(now); + std::stringstream ss; + ss << std::put_time(std::localtime(&time_t), "%Y.%m.%d %H:%M"); + + m_logger << StringExtensions::Build("libdof (DirectOutput Framework) - Version {0}, built {1}", LIBDOF_VERSION, ss.str()) << std::endl; + m_logger << "DOF created by SwissLizard / MIT License" << std::endl; + m_logger << "Original C# version: https://github.com/DirectOutput/DirectOutput" << std::endl; + m_logger << "C++ port: https://github.com/jsm174/libdof" << std::endl; + + auto nowMs = std::chrono::system_clock::now(); + auto timeT = std::chrono::system_clock::to_time_t(nowMs); + auto ms = std::chrono::duration_cast(nowMs.time_since_epoch()) % 1000; + std::stringstream timestamp; + timestamp << std::put_time(std::localtime(&timeT), "%Y.%m.%d %H:%M:%S"); + timestamp << "." << std::setfill('0') << std::setw(3) << ms.count(); + + m_logger << timestamp.str() << "\t" << StringExtensions::Build("DirectOutput logger initialized{0}", instrumentationsEnabledNote) << std::endl; + + m_isOk = true; + + if (!m_preLogFileLog.empty()) + { + for (const auto& s : m_preLogFileLog) + m_logger << s << std::endl; + + m_preLogFileLog.clear(); + } + + m_logger.flush(); + } + catch (...) + { + m_isOk = false; + } + + m_isInitialized = true; + } + else if (!m_isInitialized) + { + m_isInitialized = true; + m_isOk = false; + } +} + +void Log::AfterInit() { m_preLogFileLog.clear(); } + +void Log::Finish() +{ + std::lock_guard lock(m_locker); + if (m_logger.is_open()) + { + Write("Logging stopped"); + m_logger.flush(); + m_logger.close(); + } + m_isOk = false; + m_isInitialized = false; + m_isEnabled = true; +} + +void Log::WriteRaw(const std::string& message) +{ + std::lock_guard lock(m_locker); + if (!m_isEnabled) + return; + + if (m_isOk && m_logger.is_open()) + { + try + { + m_logger << message << std::endl; + m_logger.flush(); + } + catch (...) + { + } + } + else if (!m_isInitialized) + { + m_preLogFileLog.push_back(message); + } +} + +void Log::Write(const std::string& message) +{ + ::DOF::Log(DOF_LogLevel_INFO, "%s", message.c_str()); + WriteToFile(message); +} + +void Log::WriteToFile(const std::string& message) +{ + if (StringExtensions::IsNullOrWhiteSpace(message)) + { + auto nowMs = std::chrono::system_clock::now(); + auto timeT = std::chrono::system_clock::to_time_t(nowMs); + auto ms = std::chrono::duration_cast(nowMs.time_since_epoch()) % 1000; + std::stringstream timestamp; + timestamp << std::put_time(std::localtime(&timeT), "%Y.%m.%d %H:%M:%S"); + timestamp << "." << std::setfill('0') << std::setw(3) << ms.count(); + WriteRaw(timestamp.str() + "\t"); + } + else + { + std::stringstream ss(message); + std::string line; + while (std::getline(ss, line)) + { + if (!line.empty() && line.back() == '\r') + line.pop_back(); + + auto nowMs = std::chrono::system_clock::now(); + auto timeT = std::chrono::system_clock::to_time_t(nowMs); + auto ms = std::chrono::duration_cast(nowMs.time_since_epoch()) % 1000; + std::stringstream timestamp; + timestamp << std::put_time(std::localtime(&timeT), "%Y.%m.%d %H:%M:%S"); + timestamp << "." << std::setfill('0') << std::setw(3) << ms.count(); + WriteRaw(timestamp.str() + "\t" + line); + } + } +} + +void Log::Error(const std::string& message) +{ + ::DOF::Log(DOF_LogLevel_ERROR, "%s", message.c_str()); + WriteToFile(StringExtensions::Build("Error: {0}", message)); +} + +void Log::Warning(const std::string& message) +{ + ::DOF::Log(DOF_LogLevel_WARN, "%s", message.c_str()); + WriteToFile(StringExtensions::Build("Warning: {0}", message)); +} + +void Log::Exception(const std::string& message) +{ + ::DOF::Log(DOF_LogLevel_ERROR, "%s", message.c_str()); + WriteToFile(StringExtensions::Build("EXCEPTION: {0}", message)); +} + +void Log::Debug(const std::string& message) +{ + ::DOF::Log(DOF_LogLevel_DEBUG, "%s", message.c_str()); + WriteToFile(StringExtensions::Build("Debug: {0}", message)); +} void Log::Once(const std::string& key, const std::string& message) { @@ -32,8 +230,30 @@ void Log::Once(const std::string& key, const std::string& message) } } -void Log::Instrumentation(const std::string& key, const std::string& message) { Debug(message); } +void Log::Instrumentation(const std::string& key, const std::string& message) +{ + bool writeMessage = m_activeInstrumentations.find("*") != m_activeInstrumentations.end(); + if (!writeMessage) + { + std::stringstream ss(key); + std::string keyItem; + bool allKeysFound = true; + + while (std::getline(ss, keyItem, ',') && allKeysFound) + { + keyItem.erase(0, keyItem.find_first_not_of(" \t")); + keyItem.erase(keyItem.find_last_not_of(" \t") + 1); + if (m_activeInstrumentations.find(keyItem) == m_activeInstrumentations.end()) + allKeysFound = false; + } + writeMessage = allKeysFound; + } -Log::Log() { } + if (writeMessage) + { + ::DOF::Log(DOF_LogLevel_DEBUG, "%s", message.c_str()); + WriteToFile(StringExtensions::Build("Debug [{0}]: {1}", key, message)); + } +} } diff --git a/src/Log.h b/src/Log.h index ca86101..022acc3 100644 --- a/src/Log.h +++ b/src/Log.h @@ -2,6 +2,9 @@ #include "DOF/DOF.h" #include +#include +#include +#include namespace DOF { @@ -9,16 +12,18 @@ namespace DOF class Log { public: - static const std::string& GetFilename(); + static const std::string& GetFilename() { return m_filename; } static void SetFilename(const std::string& filename) { m_filename = filename; } - static void Init(); + static std::string GetInstrumentations() { return m_instrumentations; } + static void SetInstrumentations(const std::string& instrumentations); + + static void Init(bool enableLogging = true); static void AfterInit(); static void Finish(); - static void WriteRaw(const char* format, ...); static void Write(const std::string& message); - static void Warning(const std::string& message); static void Error(const std::string& message); + static void Warning(const std::string& message); static void Exception(const std::string& message); static void Debug(const std::string& message); static void Once(const std::string& key, const std::string& message); @@ -28,7 +33,19 @@ class Log Log(); ~Log() { }; + static void WriteRaw(const std::string& message); + static void WriteToFile(const std::string& message); + + static std::ofstream m_logger; + static bool m_isInitialized; + static bool m_isOk; + static bool m_isEnabled; + static std::mutex m_locker; + static std::string m_filename; + static std::string m_instrumentations; + static std::unordered_set m_activeInstrumentations; + static std::vector m_preLogFileLog; static std::unordered_set m_onceKeys; }; diff --git a/src/Pinball.cpp b/src/Pinball.cpp index 0f07f5e..d826566 100644 --- a/src/Pinball.cpp +++ b/src/Pinball.cpp @@ -82,9 +82,12 @@ void Pinball::Setup(const std::string& globalConfigFileName, const std::string& throw std::runtime_error("DirectOutput framework could not initialize global config."); } - if (m_globalConfig->IsEnableLogging()) + try { - if (m_globalConfig->IsClearLogOnSessionStart()) + Log::SetFilename(m_globalConfig->GetLogFilename(!StringExtensions::IsNullOrWhiteSpace(tableFilename) ? FileInfo(tableFilename).FullName() : "", romName)); + Log::SetInstrumentations(m_globalConfig->GetInstrumentation()); + + if (m_globalConfig->IsEnableLogging() && m_globalConfig->IsClearLogOnSessionStart()) { try { @@ -97,15 +100,12 @@ void Pinball::Setup(const std::string& globalConfigFileName, const std::string& { } } - try - { - Log::SetFilename(m_globalConfig->GetLogFilename(!StringExtensions::IsNullOrWhiteSpace(tableFilename) ? FileInfo(tableFilename).FullName() : "", romName)); - Log::Init(); - } - catch (const std::exception& e) - { - throw std::runtime_error("DirectOutput framework could initialize the log file."); - } + + Log::Init(m_globalConfig->IsEnableLogging()); + } + catch (const std::exception& e) + { + throw std::runtime_error("DirectOutput framework could initialize the log file."); } Log::AfterInit(); diff --git a/src/fx/AssignedEffect.cpp b/src/fx/AssignedEffect.cpp index 0e6c1d3..cd1da8b 100644 --- a/src/fx/AssignedEffect.cpp +++ b/src/fx/AssignedEffect.cpp @@ -48,19 +48,10 @@ void AssignedEffect::ResolveEffectName(Table* table) return; } - Log::Debug(StringExtensions::Build("AssignedEffect: Looking for effect '{0}' in list of {1} effects", m_effectName, std::to_string(static_cast(effects->size())))); - if (effects->size() <= 10) - { - for (const auto& pair : *effects) - { - Log::Debug(StringExtensions::Build("AssignedEffect: Available effect: '{0}'", pair.first)); - } - } auto it = effects->find(m_effectName); if (it != effects->end()) { m_effect = it->second; - Log::Debug(StringExtensions::Build("AssignedEffect: Resolved effect '{0}' successfully", m_effectName)); } else { @@ -72,12 +63,9 @@ void AssignedEffect::Trigger(TableElementData tableElementData) { if (m_effect != nullptr) { - Log::Debug(StringExtensions::Build("AssignedEffect::Trigger: Triggering effect '{0}' for element {1}{2} with value {3}", m_effect->GetName(), - std::string(1, (char)tableElementData.m_tableElementType), std::to_string(tableElementData.m_number), std::to_string(tableElementData.m_value))); try { m_effect->Trigger(&tableElementData); - Log::Debug(StringExtensions::Build("AssignedEffect::Trigger: Effect '{0}' completed", m_effect->GetName())); } catch (const std::exception& ex) { diff --git a/src/fx/EffectEffectBase.cpp b/src/fx/EffectEffectBase.cpp index a70fa81..973be2b 100644 --- a/src/fx/EffectEffectBase.cpp +++ b/src/fx/EffectEffectBase.cpp @@ -29,11 +29,9 @@ void EffectEffectBase::TriggerTargetEffect(TableElementData* triggerData) { if (m_targetEffect != nullptr) { - Log::Debug(StringExtensions::Build("EffectEffectBase::TriggerTargetEffect: Calling target effect '{0}'", m_targetEffect->GetName())); try { m_targetEffect->Trigger(triggerData); - Log::Debug(StringExtensions::Build("EffectEffectBase::TriggerTargetEffect: Target effect '{0}' completed", m_targetEffect->GetName())); } catch (const std::exception& e) { diff --git a/src/fx/conditionfx/TableElementConditionEffect.cpp b/src/fx/conditionfx/TableElementConditionEffect.cpp index 45267d0..e28b0ab 100644 --- a/src/fx/conditionfx/TableElementConditionEffect.cpp +++ b/src/fx/conditionfx/TableElementConditionEffect.cpp @@ -110,12 +110,12 @@ void TableElementConditionEffect::InitCondition() if (m_parser->compile(transformedCondition, *m_expression)) { m_conditionExpressionValid = true; - Log::Write(StringExtensions::Build("Successfully compiled expression: {0}", m_condition)); } else { m_conditionExpressionValid = false; - Log::Exception(StringExtensions::Build("Failed to compile expression {0}: {1}", m_condition, m_parser->error())); + Log::Exception(StringExtensions::Build( + "A exception has occurred while compiling the condition {0} (internally translated to {1}) of effect {2}.", m_condition, transformedCondition, NamedItemBase::GetName())); } } @@ -219,7 +219,7 @@ void TableElementConditionEffect::Trigger(TableElementData* tableElementData) } catch (const std::exception& e) { - Log::Exception(StringExtensions::Build("A exception occured when evaluating the expression {0} of effect {1}. Effect will be deactivated.", m_condition, NamedItemBase::GetName())); + Log::Exception(StringExtensions::Build("A exception occurred when evaluating the expression {0} of effect {1}. Effect will be deactivated.", m_condition, NamedItemBase::GetName())); m_conditionExpressionValid = false; } } diff --git a/src/fx/matrixfx/bitmapshapes/ShapeDefinitions.cpp b/src/fx/matrixfx/bitmapshapes/ShapeDefinitions.cpp index 9299ad8..735f39a 100644 --- a/src/fx/matrixfx/bitmapshapes/ShapeDefinitions.cpp +++ b/src/fx/matrixfx/bitmapshapes/ShapeDefinitions.cpp @@ -118,7 +118,7 @@ ShapeDefinitions* ShapeDefinitions::GetShapeDefinitionsFromShapeDefinitionsXml(c tinyxml2::XMLError result = doc.Parse(shapeDefinitionsXml.c_str()); if (result != tinyxml2::XML_SUCCESS) { - Log::Exception(StringExtensions::Build("Could not deserialize the ShapeDefinitions from XML data. Error: {0}", doc.ErrorStr() ? doc.ErrorStr() : "unknown error")); + Log::Exception("Could not load ShapeDefinitions from XML data."); throw std::runtime_error("Could not deserialize the ShapeDefinitions from XML data."); } diff --git a/src/globalconfiguration/GlobalConfig.cpp b/src/globalconfiguration/GlobalConfig.cpp index c187ffe..e77899c 100644 --- a/src/globalconfiguration/GlobalConfig.cpp +++ b/src/globalconfiguration/GlobalConfig.cpp @@ -34,6 +34,7 @@ GlobalConfig::GlobalConfig() , m_pacLedDefaultMinCommandIntervalMs(10) , m_enableLog(true) , m_clearLogOnSessionStart(true) + , m_instrumentation("") { m_tableConfigFilePatterns.clear(); m_logFilePattern.SetPattern("./DirectOutput.log"); @@ -468,6 +469,10 @@ std::string GlobalConfig::ToXml() const element->SetText(m_logFilePattern.GetPattern().c_str()); doc.FirstChildElement("GlobalConfig")->InsertEndChild(element); + element = doc.NewElement("Instrumentation"); + element->SetText(m_instrumentation.c_str()); + doc.FirstChildElement("GlobalConfig")->InsertEndChild(element); + tinyxml2::XMLPrinter printer; doc.Print(&printer); return std::string(printer.CStr()); @@ -568,6 +573,10 @@ GlobalConfig* GlobalConfig::FromXml(const std::string& configXml) if (element && element->GetText()) globalConfig->SetLogFilePattern(element->GetText()); + element = root->FirstChildElement("Instrumentation"); + if (element && element->GetText()) + globalConfig->SetInstrumentation(element->GetText()); + return globalConfig; } diff --git a/src/globalconfiguration/GlobalConfig.h b/src/globalconfiguration/GlobalConfig.h index 3e230a2..5281ddd 100644 --- a/src/globalconfiguration/GlobalConfig.h +++ b/src/globalconfiguration/GlobalConfig.h @@ -47,6 +47,8 @@ class GlobalConfig const FilePattern& GetLogFilePattern() const { return m_logFilePattern; } void SetLogFilePattern(const std::string& pattern) { m_logFilePattern.SetPattern(pattern); } std::string GetLogFilename(const std::string& tableFilename = "", const std::string& romName = "") const; + const std::string& GetInstrumentation() const { return m_instrumentation; } + void SetInstrumentation(const std::string& instrumentation) { m_instrumentation = instrumentation; } std::unordered_map GetReplaceValuesDictionary(const std::string& tableFilename = "", const std::string& romName = "") const; std::string GlobalConfigDirectoryName() const; DirectoryInfo GetGlobalConfigDirectory() const; @@ -73,6 +75,7 @@ class GlobalConfig bool m_enableLog; bool m_clearLogOnSessionStart; FilePattern m_logFilePattern; + std::string m_instrumentation; std::string m_globalConfigFileName; }; diff --git a/src/pinballsupport/AlarmHandler.cpp b/src/pinballsupport/AlarmHandler.cpp index ce48fbc..5bfecc8 100644 --- a/src/pinballsupport/AlarmHandler.cpp +++ b/src/pinballsupport/AlarmHandler.cpp @@ -26,13 +26,8 @@ void AlarmHandler::RegisterAlarm(int durationMs, AlarmCallback alarmHandler, boo { std::lock_guard lock(m_alarmMutex); - Log::Debug(StringExtensions::Build("AlarmHandler::RegisterAlarm: duration={0}ms, dontUnregister={1}, currentAlarms={2}", std::to_string(durationMs), dontUnregister ? "true" : "false", - std::to_string(static_cast(m_alarmList.size())))); - TimePoint alarmTime = std::chrono::steady_clock::now() + std::chrono::milliseconds(durationMs); m_alarmList.emplace_back(alarmTime, alarmHandler, dontUnregister); - - Log::Debug(StringExtensions::Build("AlarmHandler::RegisterAlarm: alarm registered, totalAlarms={0}", std::to_string(static_cast(m_alarmList.size())))); } void AlarmHandler::UnregisterAlarm(AlarmCallback alarmHandler) @@ -44,7 +39,6 @@ void AlarmHandler::UnregisterAlarm(AlarmCallback alarmHandler) { if (it->alarmHandler.target() == alarmHandler.target()) { - Log::Debug(StringExtensions::Build("AlarmHandler::UnregisterAlarm: removing alarm, remaining={0}", std::to_string(static_cast(m_alarmList.size() - 1)))); it = m_alarmList.erase(it); } else @@ -65,10 +59,6 @@ bool AlarmHandler::ExecuteAlarms(TimePoint alarmTime) { bool result = ProcessAlarms(alarmTime); bool intervalResult = ProcessIntervalAlarms(alarmTime); - if (result || intervalResult) - { - Log::Debug("AlarmHandler::ExecuteAlarms: Processed alarms"); - } return result || intervalResult; } @@ -101,23 +91,14 @@ bool AlarmHandler::ProcessAlarms(TimePoint alarmTime) bool alarmsExecuted = !toExecute.empty(); - if (alarmsExecuted) - { - Log::Debug(StringExtensions::Build( - "AlarmHandler::ProcessAlarms: executing {0} alarms, {1} remaining", std::to_string(static_cast(toExecute.size())), std::to_string(static_cast(m_alarmList.size())))); - } - for (const auto& alarm : toExecute) { try { - Log::Debug("AlarmHandler::ProcessAlarms: calling alarm function"); alarm.alarmHandler(); - Log::Debug("AlarmHandler::ProcessAlarms: alarm function completed"); } catch (...) { - Log::Debug("AlarmHandler::ProcessAlarms: alarm function threw exception"); } } @@ -130,9 +111,6 @@ void AlarmHandler::RegisterIntervalAlarm(int intervalMs, void* owner, AlarmCallb UnregisterIntervalAlarm(owner); m_intervalAlarmList.emplace_back(intervalMs, intervalAlarmHandler, owner); - - Log::Debug(StringExtensions::Build("AlarmHandler::RegisterIntervalAlarm: interval={0}ms, owner={1}, totalIntervalAlarms={2}", std::to_string(intervalMs), std::to_string((uintptr_t)owner), - std::to_string(static_cast(m_intervalAlarmList.size())))); } void AlarmHandler::UnregisterIntervalAlarm(void* owner) @@ -144,8 +122,6 @@ void AlarmHandler::UnregisterIntervalAlarm(void* owner) { if (it->owner == owner) { - Log::Debug(StringExtensions::Build("AlarmHandler::UnregisterIntervalAlarm: removing alarm for owner={0}, remaining={1}", std::to_string((uintptr_t)owner), - std::to_string(static_cast(m_intervalAlarmList.size() - 1)))); it = m_intervalAlarmList.erase(it); } else @@ -190,22 +166,14 @@ bool AlarmHandler::ProcessIntervalAlarms(TimePoint alarmTime) bool alarmsExecuted = !toExecute.empty(); - if (alarmsExecuted) - { - Log::Debug(StringExtensions::Build("AlarmHandler::ProcessIntervalAlarms: executing {0} interval alarms", std::to_string(static_cast(toExecute.size())))); - } - for (const auto& alarmHandler : toExecute) { try { - Log::Debug("AlarmHandler::ProcessIntervalAlarms: calling interval alarm function"); alarmHandler(); - Log::Debug("AlarmHandler::ProcessIntervalAlarms: interval alarm function completed"); } catch (...) { - Log::Debug("AlarmHandler::ProcessIntervalAlarms: interval alarm function threw exception"); } } diff --git a/src/table/TableElementList.cpp b/src/table/TableElementList.cpp index adc5ed3..425eb08 100644 --- a/src/table/TableElementList.cpp +++ b/src/table/TableElementList.cpp @@ -56,9 +56,6 @@ void TableElementList::UpdateState(TableElementData* data) if (element && element->GetTableElementType() == data->m_tableElementType && element->GetNumber() == data->m_number) { targetElement = element; - Log::Debug(StringExtensions::Build("Found matching element: type={0}, number={1}", std::string(1, (char)element->GetTableElementType()), std::to_string(element->GetNumber()))); - Log::Debug(StringExtensions::Build("TableElement - type: {0}, number: {1}, name: {2}, value: {3}", std::string(1, (char)element->GetTableElementType()), - std::to_string(element->GetNumber()), element->GetName(), std::to_string(data->m_value))); break; } }