From 404a5be3c1ac8d7b81b0876a9ec76b8001fe8c8d Mon Sep 17 00:00:00 2001 From: Duri Date: Thu, 19 Mar 2026 14:06:04 +0100 Subject: [PATCH 1/2] Add GMCP Clock Broadcasting Feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GMCP Protocol Infrastructure: GmcpModule.h - Added MUME_TIME module type (module count 7 → 8) GmcpMessage.h - Added MUME_TIME_INFO message type (message count 30 → 31) Configuration: configuration.h - Added gmcpBroadcast (default: true) and gmcpBroadcastInterval (default: 2500ms) to MumeClockSettings configuration.cpp - Added read/write persistence for the two new settings Telnet Callback Pipeline: UserTelnet.h - Added onGmcpModuleEnabled() / virt_onGmcpModuleEnabled() to UserTelnetOutputs interface UserTelnet.cpp - Calls m_outputs.onGmcpModuleEnabled() when a supported module is enabled/disabled Proxy Broadcaster: proxy.h - Added m_clockBroadcastTimer, startClockBroadcaster(), stopClockBroadcaster(), broadcastClockInfo() proxy.cpp - Full implementation: virt_onGmcpModuleEnabled callback in LocalUserTelnetOutputs starts/stops broadcaster when MUME_TIME module is toggled startClockBroadcaster() creates a QTimer that fires at the configured interval broadcastClockInfo() builds a JSON payload with year, month, day, hour, minute, precision, season, timeOfDay, moonPhase, moonVisibility, moonLevel, dawnHour, duskHour, syncEpoch stopClockBroadcaster() in destructor for cleanup Config Dialog (Mume Protocol Page): mumeprotocolpage.h - Added slots for checkbox and spinbox mumeprotocolpage.cpp - Signal connections, config loading, and slot implementations mumeprotocolpage.ui - Added "GMCP Clock Broadcasting" group box with: Checkbox: "Broadcast clock to connected clients (GMCP MUME.Time)" SpinBox: Update interval 500-60000ms, step 500, default 2500 How it works: Client connects and negotiates GMCP, requesting MUME.Time 1 UserTelnet detects the module enable and calls onGmcpModuleEnabled(MUME_TIME, true) Proxy's startClockBroadcaster() creates a timer that fires every 2500ms (configurable) Each tick builds a JSON object from MumeClock data and sends it as MUME.Time.Info via GMCP to the user client --- src/configuration/configuration.cpp | 6 + src/configuration/configuration.h | 2 + src/opengl/OpenGLProber.cpp | 1 + src/preferences/mumeprotocolpage.cpp | 25 ++++ src/preferences/mumeprotocolpage.h | 2 + src/preferences/mumeprotocolpage.ui | 45 +++++++ src/proxy/GmcpMessage.h | 3 +- src/proxy/GmcpModule.h | 3 +- src/proxy/UserTelnet.cpp | 2 + src/proxy/UserTelnet.h | 5 + src/proxy/proxy.cpp | 168 +++++++++++++++++++++++++++ src/proxy/proxy.h | 8 ++ 12 files changed, 268 insertions(+), 2 deletions(-) diff --git a/src/configuration/configuration.cpp b/src/configuration/configuration.cpp index 7e3599dfb..72ef5ed2e 100644 --- a/src/configuration/configuration.cpp +++ b/src/configuration/configuration.cpp @@ -273,6 +273,8 @@ ConstString KEY_AUDIO_OUTPUT_DEVICE = "Audio output device"; ConstString KEY_MAXIMUM_NUMBER_OF_PATHS = "maximum number of paths"; ConstString KEY_MULTIPLE_CONNECTIONS_PENALTY = "multiple connections penalty"; ConstString KEY_MUME_START_EPOCH = "Mume start epoch"; +ConstString KEY_GMCP_BROADCAST_CLOCK = "GMCP broadcast clock"; +ConstString KEY_GMCP_BROADCAST_INTERVAL = "GMCP broadcast interval"; ConstString KEY_NUMBER_OF_ANTI_ALIASING_SAMPLES = "Number of anti-aliasing samples"; ConstString KEY_PROXY_CONNECTION_STATUS = "Proxy connection status"; ConstString KEY_PROXY_LISTENS_ON_ANY_INTERFACE = "Proxy listens on any interface"; @@ -752,6 +754,8 @@ void Configuration::MumeClockSettings::read(const QSettings &conf) // NOTE: old values might be stored as int32 startEpoch = conf.value(KEY_MUME_START_EPOCH, 1517443173).toLongLong(); display = conf.value(KEY_DISPLAY_CLOCK, true).toBool(); + gmcpBroadcast = conf.value(KEY_GMCP_BROADCAST_CLOCK, true).toBool(); + gmcpBroadcastInterval = conf.value(KEY_GMCP_BROADCAST_INTERVAL, 2500).toInt(); } void Configuration::AdventurePanelSettings::read(const QSettings &conf) @@ -933,6 +937,8 @@ void Configuration::MumeClockSettings::write(QSettings &conf) const // Note: There's no QVariant(int64_t) constructor. conf.setValue(KEY_MUME_START_EPOCH, static_cast(startEpoch)); conf.setValue(KEY_DISPLAY_CLOCK, display); + conf.setValue(KEY_GMCP_BROADCAST_CLOCK, gmcpBroadcast); + conf.setValue(KEY_GMCP_BROADCAST_INTERVAL, gmcpBroadcastInterval); } void Configuration::AdventurePanelSettings::write(QSettings &conf) const diff --git a/src/configuration/configuration.h b/src/configuration/configuration.h index bd535c640..6ade92f19 100644 --- a/src/configuration/configuration.h +++ b/src/configuration/configuration.h @@ -311,6 +311,8 @@ class NODISCARD Configuration final { int64_t startEpoch = 0; bool display = false; + bool gmcpBroadcast = true; + int gmcpBroadcastInterval = 2500; private: SUBGROUP(); diff --git a/src/opengl/OpenGLProber.cpp b/src/opengl/OpenGLProber.cpp index c0160bf3b..64e76c499 100644 --- a/src/opengl/OpenGLProber.cpp +++ b/src/opengl/OpenGLProber.cpp @@ -13,6 +13,7 @@ #include #ifdef WIN32 +#include extern "C" { // Prefer discrete nVidia and AMD GPUs by default on Windows __declspec(dllexport) DWORD NvOptimusEnablement = 0x00000001; diff --git a/src/preferences/mumeprotocolpage.cpp b/src/preferences/mumeprotocolpage.cpp index b1016b09c..4d45d710d 100644 --- a/src/preferences/mumeprotocolpage.cpp +++ b/src/preferences/mumeprotocolpage.cpp @@ -30,6 +30,14 @@ MumeProtocolPage::MumeProtocolPage(QWidget *parent) &QAbstractButton::clicked, this, &MumeProtocolPage::slot_externalEditorBrowseButtonClicked); + connect(ui->gmcpBroadcastCheckBox, + &QCheckBox::stateChanged, + this, + &MumeProtocolPage::slot_gmcpBroadcastCheckBoxChanged); + connect(ui->gmcpIntervalSpinBox, + QOverload::of(&QSpinBox::valueChanged), + this, + &MumeProtocolPage::slot_gmcpIntervalSpinBoxChanged); } MumeProtocolPage::~MumeProtocolPage() @@ -49,6 +57,11 @@ void MumeProtocolPage::slot_loadConfig() if constexpr (CURRENT_PLATFORM == PlatformEnum::Wasm) { ui->externalEditorRadioButton->setDisabled(true); } + + const auto &clockSettings = getConfig().mumeClock; + ui->gmcpBroadcastCheckBox->setChecked(clockSettings.gmcpBroadcast); + ui->gmcpIntervalSpinBox->setValue(clockSettings.gmcpBroadcastInterval); + ui->gmcpIntervalSpinBox->setEnabled(clockSettings.gmcpBroadcast); } void MumeProtocolPage::slot_internalEditorRadioButtonChanged(bool /*unused*/) @@ -78,3 +91,15 @@ void MumeProtocolPage::slot_externalEditorBrowseButtonClicked(bool /*unused*/) command = quotedFileName; } } + +void MumeProtocolPage::slot_gmcpBroadcastCheckBoxChanged(int /*unused*/) +{ + const bool enabled = ui->gmcpBroadcastCheckBox->isChecked(); + setConfig().mumeClock.gmcpBroadcast = enabled; + ui->gmcpIntervalSpinBox->setEnabled(enabled); +} + +void MumeProtocolPage::slot_gmcpIntervalSpinBoxChanged(int value) +{ + setConfig().mumeClock.gmcpBroadcastInterval = value; +} diff --git a/src/preferences/mumeprotocolpage.h b/src/preferences/mumeprotocolpage.h index 583d528d9..d39411f93 100644 --- a/src/preferences/mumeprotocolpage.h +++ b/src/preferences/mumeprotocolpage.h @@ -31,4 +31,6 @@ public slots: void slot_internalEditorRadioButtonChanged(bool); void slot_externalEditorCommandTextChanged(QString); void slot_externalEditorBrowseButtonClicked(bool); + void slot_gmcpBroadcastCheckBoxChanged(int); + void slot_gmcpIntervalSpinBoxChanged(int); }; diff --git a/src/preferences/mumeprotocolpage.ui b/src/preferences/mumeprotocolpage.ui index e509334ef..1ed062d61 100644 --- a/src/preferences/mumeprotocolpage.ui +++ b/src/preferences/mumeprotocolpage.ui @@ -120,6 +120,51 @@ + + + + GMCP Clock Broadcasting + + + + + + Broadcast clock to connected clients (GMCP MUME.Time) + + + true + + + + + + + Update interval (ms): + + + + + + + How often to send clock updates to clients (default: 2500ms = 1 MUME minute) + + + 500 + + + 60000 + + + 500 + + + 2500 + + + + + + diff --git a/src/proxy/GmcpMessage.h b/src/proxy/GmcpMessage.h index 806e3b4a1..9e172e558 100644 --- a/src/proxy/GmcpMessage.h +++ b/src/proxy/GmcpMessage.h @@ -54,6 +54,7 @@ class ParseEvent; X(ROOM_CHARS_UPDATE, RoomCharsUpdate, "room.chars.update", "Room.Chars.Update") \ X(ROOM_INFO, RoomInfo, "room.info", "Room.Info") \ X(ROOM_UPDATE_EXITS, RoomUpdateExits, "room.update.exits", "Room.Update.Exits") \ + X(MUME_TIME_INFO, MumeTimeInfo, "mume.time.info", "MUME.Time.Info") \ /* define gmcp message types above */ enum class NODISCARD GmcpMessageTypeEnum { @@ -66,7 +67,7 @@ enum class NODISCARD GmcpMessageTypeEnum { #define X_COUNT(...) +1 static constexpr const size_t NUM_GMCP_MESSAGES = XFOREACH_GMCP_MESSAGE_TYPE(X_COUNT); #undef X_COUNT -static_assert(NUM_GMCP_MESSAGES == 30); +static_assert(NUM_GMCP_MESSAGES == 31); DEFINE_ENUM_COUNT(GmcpMessageTypeEnum, NUM_GMCP_MESSAGES) namespace tags { diff --git a/src/proxy/GmcpModule.h b/src/proxy/GmcpModule.h index 90f03d26e..9e42f7c51 100644 --- a/src/proxy/GmcpModule.h +++ b/src/proxy/GmcpModule.h @@ -23,6 +23,7 @@ X(MUME_CLIENT, MumeClient, "mume.client", "MUME.Client") \ X(ROOM_CHARS, RoomChars, "room.chars", "Room.Chars") \ X(ROOM, Room, "room", "Room") \ + X(MUME_TIME, MumeTime, "mume.time", "MUME.Time") \ /* define gmcp module types above */ enum class NODISCARD GmcpModuleTypeEnum { @@ -35,7 +36,7 @@ enum class NODISCARD GmcpModuleTypeEnum { #define X_COUNT(...) +1 static constexpr const size_t NUM_GMCP_MODULES = XFOREACH_GMCP_MODULE_TYPE(X_COUNT); #undef X_COUNT -static_assert(NUM_GMCP_MODULES == 7); +static_assert(NUM_GMCP_MODULES == 8); DEFINE_ENUM_COUNT(GmcpModuleTypeEnum, NUM_GMCP_MODULES) namespace tags { diff --git a/src/proxy/UserTelnet.cpp b/src/proxy/UserTelnet.cpp index e02e2fdb3..45f78c5ff 100644 --- a/src/proxy/UserTelnet.cpp +++ b/src/proxy/UserTelnet.cpp @@ -261,12 +261,14 @@ void UserTelnet::receiveGmcpModule(const GmcpModule &mod, const bool enabled) m_gmcp.modules.insert(mod); if (mod.isSupported()) { m_gmcp.supported[mod.getType()] = mod.getVersion(); + m_outputs.onGmcpModuleEnabled(mod.getType(), true); } } else { m_gmcp.modules.erase(mod); if (mod.isSupported()) { m_gmcp.supported[mod.getType()] = DEFAULT_GMCP_MODULE_VERSION; + m_outputs.onGmcpModuleEnabled(mod.getType(), false); } } } diff --git a/src/proxy/UserTelnet.h b/src/proxy/UserTelnet.h index 8cf7f4fbd..2692e5ea5 100644 --- a/src/proxy/UserTelnet.h +++ b/src/proxy/UserTelnet.h @@ -29,6 +29,10 @@ struct NODISCARD UserTelnetOutputs { virt_onRelayTermTypeFromUserToMud(bytes); } + void onGmcpModuleEnabled(const GmcpModuleTypeEnum type, const bool enabled) + { + virt_onGmcpModuleEnabled(type, enabled); + } private: virtual void virt_onAnalyzeUserStream(const RawBytes &, bool) = 0; @@ -36,6 +40,7 @@ struct NODISCARD UserTelnetOutputs virtual void virt_onRelayGmcpFromUserToMud(const GmcpMessage &) = 0; virtual void virt_onRelayNawsFromUserToMud(int, int) = 0; virtual void virt_onRelayTermTypeFromUserToMud(const TelnetTermTypeBytes &) = 0; + virtual void virt_onGmcpModuleEnabled(GmcpModuleTypeEnum, bool) = 0; }; class NODISCARD UserTelnet final : public AbstractTelnet diff --git a/src/proxy/proxy.cpp b/src/proxy/proxy.cpp index 78a62f3bd..93142c4f7 100644 --- a/src/proxy/proxy.cpp +++ b/src/proxy/proxy.cpp @@ -41,11 +41,14 @@ #include #include +#include +#include #include #include #include #include #include +#include using mmqt::makeQPointer; @@ -177,6 +180,8 @@ Proxy::~Proxy() sendNewlineToUser(); sendStatusToUser("MMapper proxy is shutting down."); + stopClockBroadcaster(); + { qDebug() << "disconnecting mud socket..."; getMudSocket().disconnectFromHost(); @@ -408,6 +413,16 @@ void Proxy::allocUserTelnet() // forwarded (to mud) getMudTelnet().onRelayTermType(bytes); } + void virt_onGmcpModuleEnabled(const GmcpModuleTypeEnum type, const bool enabled) final + { + if (type == GmcpModuleTypeEnum::MUME_TIME) { + if (enabled) { + getProxy().startClockBroadcaster(); + } else { + getProxy().stopClockBroadcaster(); + } + } + } }; auto &pipe = getPipeline(); @@ -884,6 +899,159 @@ void Proxy::gmcpToUser(const GmcpMessage &msg) getUserTelnet().onGmcpToUser(msg); } +void Proxy::startClockBroadcaster() +{ + const auto &config = getConfig(); + + if (!config.mumeClock.gmcpBroadcast + || !isUserGmcpModuleEnabled(GmcpModuleTypeEnum::MUME_TIME)) { + return; + } + + if (m_clockBroadcastTimer == nullptr) { + m_clockBroadcastTimer = new QTimer(this); + QObject::connect(m_clockBroadcastTimer, &QTimer::timeout, this, &Proxy::broadcastClockInfo); + } + + m_clockBroadcastTimer->setInterval(config.mumeClock.gmcpBroadcastInterval); + m_clockBroadcastTimer->start(); + + // Send initial update immediately + broadcastClockInfo(); +} + +void Proxy::stopClockBroadcaster() +{ + if (m_clockBroadcastTimer != nullptr && m_clockBroadcastTimer->isActive()) { + m_clockBroadcastTimer->stop(); + } +} + +void Proxy::broadcastClockInfo() +{ + if (!isUserGmcpModuleEnabled(GmcpModuleTypeEnum::MUME_TIME)) { + return; + } + + const MumeMoment moment = m_mumeClock.getMumeMoment(); + const auto precision = m_mumeClock.getPrecision(); + const auto [dawnHour, duskHour] = MumeClock::getDawnDusk(moment.month); + + QJsonObject json; + json["year"] = moment.year; + json["month"] = moment.month; + json["day"] = moment.day; + json["hour"] = moment.hour; + json["minute"] = moment.minute; + + switch (precision) { + case MumeClockPrecisionEnum::UNSET: + json["precision"] = "unset"; + break; + case MumeClockPrecisionEnum::DAY: + json["precision"] = "day"; + break; + case MumeClockPrecisionEnum::HOUR: + json["precision"] = "hour"; + break; + case MumeClockPrecisionEnum::MINUTE: + json["precision"] = "minute"; + break; + } + + switch (moment.toSeason()) { + case MumeSeasonEnum::WINTER: + json["season"] = "winter"; + break; + case MumeSeasonEnum::SPRING: + json["season"] = "spring"; + break; + case MumeSeasonEnum::SUMMER: + json["season"] = "summer"; + break; + case MumeSeasonEnum::AUTUMN: + json["season"] = "autumn"; + break; + case MumeSeasonEnum::UNKNOWN: + json["season"] = "unknown"; + break; + } + + switch (moment.toTimeOfDay()) { + case MumeTimeEnum::DAWN: + json["timeOfDay"] = "dawn"; + break; + case MumeTimeEnum::DAY: + json["timeOfDay"] = "day"; + break; + case MumeTimeEnum::DUSK: + json["timeOfDay"] = "dusk"; + break; + case MumeTimeEnum::NIGHT: + json["timeOfDay"] = "night"; + break; + case MumeTimeEnum::UNKNOWN: + json["timeOfDay"] = "unknown"; + break; + } + + switch (moment.moonPhase()) { + case MumeMoonPhaseEnum::NEW_MOON: + json["moonPhase"] = "new_moon"; + break; + case MumeMoonPhaseEnum::WAXING_CRESCENT: + json["moonPhase"] = "waxing_crescent"; + break; + case MumeMoonPhaseEnum::FIRST_QUARTER: + json["moonPhase"] = "first_quarter"; + break; + case MumeMoonPhaseEnum::WAXING_GIBBOUS: + json["moonPhase"] = "waxing_gibbous"; + break; + case MumeMoonPhaseEnum::FULL_MOON: + json["moonPhase"] = "full_moon"; + break; + case MumeMoonPhaseEnum::WANING_GIBBOUS: + json["moonPhase"] = "waning_gibbous"; + break; + case MumeMoonPhaseEnum::THIRD_QUARTER: + json["moonPhase"] = "third_quarter"; + break; + case MumeMoonPhaseEnum::WANING_CRESCENT: + json["moonPhase"] = "waning_crescent"; + break; + case MumeMoonPhaseEnum::UNKNOWN: + json["moonPhase"] = "unknown"; + break; + } + + switch (moment.moonVisibility()) { + case MumeMoonVisibilityEnum::BRIGHT: + json["moonVisibility"] = "bright"; + break; + case MumeMoonVisibilityEnum::DIM: + json["moonVisibility"] = "dim"; + break; + case MumeMoonVisibilityEnum::INVISIBLE: + json["moonVisibility"] = "invisible"; + break; + case MumeMoonVisibilityEnum::UNKNOWN: + json["moonVisibility"] = "unknown"; + break; + } + + json["moonLevel"] = moment.moonLevel(); + json["dawnHour"] = dawnHour; + json["duskHour"] = duskHour; + json["syncEpoch"] = static_cast(m_mumeClock.getLastSyncEpoch()); + + const QJsonDocument doc(json); + const QString jsonString = QString::fromUtf8(doc.toJson(QJsonDocument::Compact)); + + const GmcpMessage msg(GmcpMessageTypeEnum::MUME_TIME_INFO, GmcpJson{jsonString.toStdString()}); + gmcpToUser(msg); +} + void Proxy::sendToMud(const QString &s) { // REVISIT: this bypasses game observer, but it also appears to be unused. diff --git a/src/proxy/proxy.h b/src/proxy/proxy.h index 3bb78a08f..11213cb54 100644 --- a/src/proxy/proxy.h +++ b/src/proxy/proxy.h @@ -153,6 +153,9 @@ class NODISCARD_QOBJECT Proxy final : public QObject // it outlives this object when the connection closes. QPointer m_remoteEdit; + // Clock GMCP broadcaster + QTimer *m_clockBroadcastTimer = nullptr; + enum class NODISCARD ServerStateEnum { Initialized, Offline, @@ -227,6 +230,11 @@ class NODISCARD_QOBJECT Proxy final : public QObject void gmcpToMud(const GmcpMessage &msg); void gmcpToUser(const GmcpMessage &msg); +private: + void startClockBroadcaster(); + void stopClockBroadcaster(); + void broadcastClockInfo(); + private: void sendToMud(const QString &s); void sendToUser(SendToUserSourceEnum source, const QString &ba); From f1724abf4e538002d152a9455bc30ef9e1312dc6 Mon Sep 17 00:00:00 2001 From: Duri Date: Thu, 19 Mar 2026 16:19:51 +0100 Subject: [PATCH 2/2] Fix GMCP Clock Broadcasting interval and start/stop Interval clamping (proxy.cpp:983-987): std::clamp bounds the interval to 500-60000ms, preventing tight timer loops from corrupt/manual config edits. Runtime config check (proxy.cpp:1005): broadcastClockInfo() now checks gmcpBroadcast config setting, so disabling broadcast in preferences immediately halts updates even if the timer is still running. Stop on config disable (proxy.cpp:908): startClockBroadcaster() now calls stopClockBroadcaster() when the config is disabled, so re-evaluating the broadcaster properly stops it. Enum-to-string helpers (proxy.cpp:902-968): Extracted 5 constexpr toJsonString() overloads in an anonymous namespace, replacing ~80 lines of verbose switch/break blocks with clean one-liners in broadcastClockInfo(). --- src/proxy/proxy.cpp | 178 ++++++++++++++++++++------------------------ 1 file changed, 81 insertions(+), 97 deletions(-) diff --git a/src/proxy/proxy.cpp b/src/proxy/proxy.cpp index 93142c4f7..d78756c47 100644 --- a/src/proxy/proxy.cpp +++ b/src/proxy/proxy.cpp @@ -899,12 +899,79 @@ void Proxy::gmcpToUser(const GmcpMessage &msg) getUserTelnet().onGmcpToUser(msg); } +namespace { + +NODISCARD constexpr const char *toJsonString(const MumeClockPrecisionEnum precision) +{ + switch (precision) { + case MumeClockPrecisionEnum::UNSET: return "unset"; + case MumeClockPrecisionEnum::DAY: return "day"; + case MumeClockPrecisionEnum::HOUR: return "hour"; + case MumeClockPrecisionEnum::MINUTE: return "minute"; + } + return "unknown"; +} + +NODISCARD constexpr const char *toJsonString(const MumeSeasonEnum season) +{ + switch (season) { + case MumeSeasonEnum::WINTER: return "winter"; + case MumeSeasonEnum::SPRING: return "spring"; + case MumeSeasonEnum::SUMMER: return "summer"; + case MumeSeasonEnum::AUTUMN: return "autumn"; + case MumeSeasonEnum::UNKNOWN: return "unknown"; + } + return "unknown"; +} + +NODISCARD constexpr const char *toJsonString(const MumeTimeEnum time) +{ + switch (time) { + case MumeTimeEnum::DAWN: return "dawn"; + case MumeTimeEnum::DAY: return "day"; + case MumeTimeEnum::DUSK: return "dusk"; + case MumeTimeEnum::NIGHT: return "night"; + case MumeTimeEnum::UNKNOWN: return "unknown"; + } + return "unknown"; +} + +NODISCARD constexpr const char *toJsonString(const MumeMoonPhaseEnum phase) +{ + switch (phase) { + case MumeMoonPhaseEnum::NEW_MOON: return "new_moon"; + case MumeMoonPhaseEnum::WAXING_CRESCENT: return "waxing_crescent"; + case MumeMoonPhaseEnum::FIRST_QUARTER: return "first_quarter"; + case MumeMoonPhaseEnum::WAXING_GIBBOUS: return "waxing_gibbous"; + case MumeMoonPhaseEnum::FULL_MOON: return "full_moon"; + case MumeMoonPhaseEnum::WANING_GIBBOUS: return "waning_gibbous"; + case MumeMoonPhaseEnum::THIRD_QUARTER: return "third_quarter"; + case MumeMoonPhaseEnum::WANING_CRESCENT: return "waning_crescent"; + case MumeMoonPhaseEnum::UNKNOWN: return "unknown"; + } + return "unknown"; +} + +NODISCARD constexpr const char *toJsonString(const MumeMoonVisibilityEnum visibility) +{ + switch (visibility) { + case MumeMoonVisibilityEnum::BRIGHT: return "bright"; + case MumeMoonVisibilityEnum::DIM: return "dim"; + case MumeMoonVisibilityEnum::INVISIBLE: return "invisible"; + case MumeMoonVisibilityEnum::UNKNOWN: return "unknown"; + } + return "unknown"; +} + +} // namespace + void Proxy::startClockBroadcaster() { const auto &config = getConfig(); if (!config.mumeClock.gmcpBroadcast || !isUserGmcpModuleEnabled(GmcpModuleTypeEnum::MUME_TIME)) { + stopClockBroadcaster(); return; } @@ -913,7 +980,13 @@ void Proxy::startClockBroadcaster() QObject::connect(m_clockBroadcastTimer, &QTimer::timeout, this, &Proxy::broadcastClockInfo); } - m_clockBroadcastTimer->setInterval(config.mumeClock.gmcpBroadcastInterval); + // Clamp interval to a sane range to prevent tight timer loops + static constexpr int MIN_INTERVAL_MS = 500; + static constexpr int MAX_INTERVAL_MS = 60000; + const int interval = std::clamp(config.mumeClock.gmcpBroadcastInterval, + MIN_INTERVAL_MS, + MAX_INTERVAL_MS); + m_clockBroadcastTimer->setInterval(interval); m_clockBroadcastTimer->start(); // Send initial update immediately @@ -929,7 +1002,8 @@ void Proxy::stopClockBroadcaster() void Proxy::broadcastClockInfo() { - if (!isUserGmcpModuleEnabled(GmcpModuleTypeEnum::MUME_TIME)) { + if (!getConfig().mumeClock.gmcpBroadcast + || !isUserGmcpModuleEnabled(GmcpModuleTypeEnum::MUME_TIME)) { return; } @@ -944,101 +1018,11 @@ void Proxy::broadcastClockInfo() json["hour"] = moment.hour; json["minute"] = moment.minute; - switch (precision) { - case MumeClockPrecisionEnum::UNSET: - json["precision"] = "unset"; - break; - case MumeClockPrecisionEnum::DAY: - json["precision"] = "day"; - break; - case MumeClockPrecisionEnum::HOUR: - json["precision"] = "hour"; - break; - case MumeClockPrecisionEnum::MINUTE: - json["precision"] = "minute"; - break; - } - - switch (moment.toSeason()) { - case MumeSeasonEnum::WINTER: - json["season"] = "winter"; - break; - case MumeSeasonEnum::SPRING: - json["season"] = "spring"; - break; - case MumeSeasonEnum::SUMMER: - json["season"] = "summer"; - break; - case MumeSeasonEnum::AUTUMN: - json["season"] = "autumn"; - break; - case MumeSeasonEnum::UNKNOWN: - json["season"] = "unknown"; - break; - } - - switch (moment.toTimeOfDay()) { - case MumeTimeEnum::DAWN: - json["timeOfDay"] = "dawn"; - break; - case MumeTimeEnum::DAY: - json["timeOfDay"] = "day"; - break; - case MumeTimeEnum::DUSK: - json["timeOfDay"] = "dusk"; - break; - case MumeTimeEnum::NIGHT: - json["timeOfDay"] = "night"; - break; - case MumeTimeEnum::UNKNOWN: - json["timeOfDay"] = "unknown"; - break; - } - - switch (moment.moonPhase()) { - case MumeMoonPhaseEnum::NEW_MOON: - json["moonPhase"] = "new_moon"; - break; - case MumeMoonPhaseEnum::WAXING_CRESCENT: - json["moonPhase"] = "waxing_crescent"; - break; - case MumeMoonPhaseEnum::FIRST_QUARTER: - json["moonPhase"] = "first_quarter"; - break; - case MumeMoonPhaseEnum::WAXING_GIBBOUS: - json["moonPhase"] = "waxing_gibbous"; - break; - case MumeMoonPhaseEnum::FULL_MOON: - json["moonPhase"] = "full_moon"; - break; - case MumeMoonPhaseEnum::WANING_GIBBOUS: - json["moonPhase"] = "waning_gibbous"; - break; - case MumeMoonPhaseEnum::THIRD_QUARTER: - json["moonPhase"] = "third_quarter"; - break; - case MumeMoonPhaseEnum::WANING_CRESCENT: - json["moonPhase"] = "waning_crescent"; - break; - case MumeMoonPhaseEnum::UNKNOWN: - json["moonPhase"] = "unknown"; - break; - } - - switch (moment.moonVisibility()) { - case MumeMoonVisibilityEnum::BRIGHT: - json["moonVisibility"] = "bright"; - break; - case MumeMoonVisibilityEnum::DIM: - json["moonVisibility"] = "dim"; - break; - case MumeMoonVisibilityEnum::INVISIBLE: - json["moonVisibility"] = "invisible"; - break; - case MumeMoonVisibilityEnum::UNKNOWN: - json["moonVisibility"] = "unknown"; - break; - } + json["precision"] = toJsonString(precision); + json["season"] = toJsonString(moment.toSeason()); + json["timeOfDay"] = toJsonString(moment.toTimeOfDay()); + json["moonPhase"] = toJsonString(moment.moonPhase()); + json["moonVisibility"] = toJsonString(moment.moonVisibility()); json["moonLevel"] = moment.moonLevel(); json["dawnHour"] = dawnHour;