From 43e3e9fe8061b62cf4029e9ebe1927507dfaaa1e Mon Sep 17 00:00:00 2001 From: Mattias Viklund <40642947+Mattias-Viklund@users.noreply.github.com> Date: Fri, 2 Feb 2024 19:24:24 +0100 Subject: [PATCH] add Mud Server Protocol (Amend) to handle game time --- src/clock/mumeclock.cpp | 34 +++++++++ src/clock/mumeclock.h | 4 +- src/clock/mumeclockwidget.cpp | 5 +- src/parser/abstractparser.h | 2 + src/proxy/AbstractTelnet.cpp | 13 ++++ src/proxy/AbstractTelnet.h | 5 ++ src/proxy/MudTelnet.cpp | 127 ++++++++++++++++++++++++++++++++++ src/proxy/MudTelnet.h | 7 ++ src/proxy/UserTelnet.cpp | 5 ++ src/proxy/UserTelnet.h | 1 + src/proxy/proxy.cpp | 19 +++++ src/proxy/proxy.h | 5 ++ tests/testclock.cpp | 54 +++++++++++++++ tests/testclock.h | 1 + 14 files changed, 279 insertions(+), 3 deletions(-) diff --git a/src/clock/mumeclock.cpp b/src/clock/mumeclock.cpp index dea45a06c..642e7e1aa 100644 --- a/src/clock/mumeclock.cpp +++ b/src/clock/mumeclock.cpp @@ -291,6 +291,31 @@ void MumeClock::parseClockTime(const QString &clockTime, const int64_t secsSince m_mumeStartEpoch = newStartEpoch; } +void MumeClock::parseMSSP(const int year, const int month, const int day, const int hour) +{ + // We should not parse the fuzzy MSSP time if we already have a greater precision. + if (getPrecision() > MumeClockPrecisionEnum::DAY) + return; + + const int64_t secsSinceEpoch = QDateTime::QDateTime::currentDateTimeUtc().toSecsSinceEpoch(); + + auto moment = getMumeMoment(); + moment.year = year; + moment.month = month; + moment.day = day; + moment.hour = hour; + // Don't override minute, since we don't get the from the MSSP time. + + const int64_t newStartEpoch = secsSinceEpoch - moment.toSeconds(); + m_mumeStartEpoch = newStartEpoch; + + // Update last sync timestamp + setLastSyncEpoch(secsSinceEpoch); + + setPrecision(MumeClockPrecisionEnum::HOUR); + log("Synchronized clock using MSSP"); +} + // TODO: move this somewhere useful? NODISCARD static const char *getOrdinalSuffix(const int day) { @@ -410,3 +435,12 @@ MumeClock::DawnDusk MumeClock::getDawnDusk(const int month) const auto m = static_cast(month); return DawnDusk{s_dawnHour.at(m), s_duskHour.at(m)}; } + +int MumeClock::getMumeMonth(const QString &monthName) +{ + const int month = s_westronMonthNames.keyToValue(monthName.toLatin1().data()); + if (month == static_cast(WestronMonthNamesEnum::UnknownWestronMonth)) { + return s_sindarinMonthNames.keysToValue(monthName.toLatin1().data()); + } + return month; +} diff --git a/src/clock/mumeclock.h b/src/clock/mumeclock.h index 71b66f508..a58236dc5 100644 --- a/src/clock/mumeclock.h +++ b/src/clock/mumeclock.h @@ -138,9 +138,11 @@ public slots: public: void setPrecision(const MumeClockPrecisionEnum state); - void setLastSyncEpoch(int64_t epoch) { m_lastSyncEpoch = epoch; } + int getMumeMonth(const QString &monthName); + void parseMSSP(const int year, const int month, const int day, const int hour); + protected: void parseMumeTime(const QString &mumeTime, int64_t secsSinceEpoch); diff --git a/src/clock/mumeclockwidget.cpp b/src/clock/mumeclockwidget.cpp index 583cfe98d..9528ec2f5 100644 --- a/src/clock/mumeclockwidget.cpp +++ b/src/clock/mumeclockwidget.cpp @@ -139,7 +139,6 @@ void MumeClockWidget::slot_updateLabel() QString statusTip = ""; if (precision <= MumeClockPrecisionEnum::UNSET) { styleSheet = "padding-left:1px;padding-right:1px;color:white;background:grey"; - statusTip = "The clock has not synced with MUME! Click to override at your own risk."; } else if (time == MumeTimeEnum::DAWN) { styleSheet = "padding-left:1px;padding-right:1px;color:white;background:red"; statusTip = "Ticks left until day"; @@ -150,11 +149,13 @@ void MumeClockWidget::slot_updateLabel() styleSheet = "padding-left:1px;padding-right:1px;color:black;background:yellow"; statusTip = "Ticks left until night"; } + if (precision != MumeClockPrecisionEnum::MINUTE) + statusTip = "The clock has not synced with MUME! Click to override at your own risk."; timeLabel->setStyleSheet(styleSheet); timeLabel->setStatusTip(statusTip); updateMoonStyleSheet = true; } - if (precision <= MumeClockPrecisionEnum::DAY) { + if (precision <= MumeClockPrecisionEnum::HOUR) { // Prepend warning emoji to countdown timeLabel->setText(QString::fromUtf8("\xe2\x9a\xa0").append(m_clock->toCountdown(moment))); } else diff --git a/src/parser/abstractparser.h b/src/parser/abstractparser.h index 4c0bf01be..26f8e983e 100644 --- a/src/parser/abstractparser.h +++ b/src/parser/abstractparser.h @@ -211,6 +211,8 @@ protected slots: void showHeader(const QString &s); + void receiveMudServerStatus(const QByteArray &); + NODISCARD ExitDirEnum tryGetDir(StringView &words); void parseSetCommand(StringView view); diff --git a/src/proxy/AbstractTelnet.cpp b/src/proxy/AbstractTelnet.cpp index 7c26d14ca..8bc17cbd6 100644 --- a/src/proxy/AbstractTelnet.cpp +++ b/src/proxy/AbstractTelnet.cpp @@ -342,6 +342,17 @@ void AbstractTelnet::sendGmcpMessage(const GmcpMessage &msg) s.addSubnegEnd(); } +void AbstractTelnet::sendMudServerStatus(const QByteArray &data) +{ + if (debug) + qDebug() << "Sending MSSP:" << data; + + TelnetFormatter s{*this}; + s.addSubnegBegin(OPT_MSSP); + s.addEscapedBytes(data); + s.addSubnegEnd(); +} + void AbstractTelnet::sendLineModeEdit() { if (debug) @@ -704,6 +715,8 @@ void AbstractTelnet::processTelnetSubnegotiation(const AppendBuffer &payload) if (hisOptionState[OPT_MSSP]) { if (debug) qDebug() << "Received MSSP message" << payload; + + receiveMudServerStatus(payload); } break; diff --git a/src/proxy/AbstractTelnet.h b/src/proxy/AbstractTelnet.h index 668f768bd..75e18302b 100644 --- a/src/proxy/AbstractTelnet.h +++ b/src/proxy/AbstractTelnet.h @@ -59,6 +59,8 @@ static constexpr const uint8_t TNSB_SEND = 1; static constexpr const uint8_t TNSB_REQUEST = 1; static constexpr const uint8_t TNSB_MODE = 1; static constexpr const uint8_t TNSB_EDIT = 1; +static constexpr const uint8_t TNSB_MSSP_VAR = 1; +static constexpr const uint8_t TNSB_MSSP_VAL = 2; static constexpr const uint8_t TNSB_ACCEPTED = 2; static constexpr const uint8_t TNSB_REJECTED = 3; static constexpr const uint8_t TNSB_TTABLE_IS = 4; @@ -122,6 +124,7 @@ class AbstractTelnet : public QObject void sendWindowSizeChanged(int, int); void sendTerminalTypeRequest(); void sendGmcpMessage(const GmcpMessage &msg); + void sendMudServerStatus(const QByteArray &); void sendLineModeEdit(); void requestTelnetOption(unsigned char type, unsigned char subnegBuffer); @@ -134,6 +137,7 @@ class AbstractTelnet : public QObject virtual void virt_receiveEchoMode(bool) {} virtual void virt_receiveGmcpMessage(const GmcpMessage &) {} virtual void virt_receiveTerminalType(const QByteArray &) {} + virtual void virt_receiveMudServerStatus(const QByteArray &) {} virtual void virt_receiveWindowSize(int, int) {} /// Send out the data. Does not double IACs, this must be done /// by caller if needed. This function is suitable for sending @@ -146,6 +150,7 @@ class AbstractTelnet : public QObject void receiveEchoMode(bool b) { virt_receiveEchoMode(b); } void receiveGmcpMessage(const GmcpMessage &msg) { virt_receiveGmcpMessage(msg); } void receiveTerminalType(const QByteArray &ba) { virt_receiveTerminalType(ba); } + void receiveMudServerStatus(const QByteArray &ba) { virt_receiveMudServerStatus(ba); } void receiveWindowSize(int x, int y) { virt_receiveWindowSize(x, y); } /// Send out the data. Does not double IACs, this must be done diff --git a/src/proxy/MudTelnet.cpp b/src/proxy/MudTelnet.cpp index fad91b1e3..8906316db 100644 --- a/src/proxy/MudTelnet.cpp +++ b/src/proxy/MudTelnet.cpp @@ -147,6 +147,12 @@ void MudTelnet::virt_receiveGmcpMessage(const GmcpMessage &msg) emit sig_relayGmcp(msg); } +void MudTelnet::virt_receiveMudServerStatus(const QByteArray &ba) +{ + parseMudServerStatus(ba); + emit sig_sendMSSPToUser(ba); +} + void MudTelnet::virt_onGmcpEnabled() { if (debug) @@ -215,3 +221,124 @@ void MudTelnet::sendCoreSupports() sendGmcpMessage(GmcpMessage(GmcpMessageTypeEnum::CORE_SUPPORTS_SET, set)); } + +void MudTelnet::parseMudServerStatus(const QByteArray &data) +{ + std::map> map; + + std::optional varName = std::nullopt; + std::list vals; + + enum class NODISCARD MSSPStateEnum { + /// + BEGIN, + /// VAR + IN_VAR, + /// VAL + IN_VAL + } state + = MSSPStateEnum::BEGIN; + + AppendBuffer buffer; + + const auto addValue([&map, &vals, &varName, &buffer, this]() { + // Put it into the map. + if (debug) + qDebug() << "MSSP received value" << ::toQByteArrayLatin1(buffer.toStdString()) + << "for variable" << ::toQByteArrayLatin1(varName.value()); + + vals.push_back(buffer.toStdString()); + map[varName.value()] = vals; + + buffer.clear(); + }); + + for (int i = 0; i < data.size(); i++) { + switch (state) { + case MSSPStateEnum::BEGIN: + if (data.at(i) != TNSB_MSSP_VAR) + continue; + state = MSSPStateEnum::IN_VAR; + break; + + case MSSPStateEnum::IN_VAR: + switch (data.at(i)) { + case TNSB_MSSP_VAR: + case TN_IAC: + case 0: + continue; + + case TNSB_MSSP_VAL: { + if (buffer.isEmpty()) { + if (debug) + qDebug() << "MSSP received variable without any name; ignoring it"; + continue; + } + + if (debug) + qDebug() << "MSSP received variable" + << ::toQByteArrayLatin1(buffer.toStdString()); + + varName = buffer.toStdString(); + state = MSSPStateEnum::IN_VAL; + + vals.clear(); // Which means this is a new value, so clear the list. + buffer.clear(); + } break; + + default: + buffer.append(static_cast(data.at(i))); + } + break; + + case MSSPStateEnum::IN_VAL: { + assert(varName.has_value()); + + switch (data.at(i)) { + case TN_IAC: + case 0: + continue; + + case TNSB_MSSP_VAR: + state = MSSPStateEnum::IN_VAR; + FALLTHROUGH; + case TNSB_MSSP_VAL: + addValue(); + break; + + default: + buffer.append(static_cast(data.at(i))); + break; + } + break; + + } break; + } + } + + if (varName.has_value() && !buffer.isEmpty()) + addValue(); + + // Parse game time from MSSP + const auto firstElement( + [](const std::list &elements) -> std::optional { + return elements.empty() ? std::nullopt : std::optional{elements.front()}; + }); + const auto yearStr = firstElement(map["GAME YEAR"]); + const auto monthStr = firstElement(map["GAME MONTH"]); + const auto dayStr = firstElement(map["GAME DAY"]); + const auto hourStr = firstElement(map["GAME HOUR"]); + + qInfo() << "MSSP game time received with" + << "year:" << ::toQByteArrayLatin1(yearStr.value_or("unknown")) + << "month:" << ::toQByteArrayLatin1(monthStr.value_or("unknown")) + << "day:" << ::toQByteArrayLatin1(dayStr.value_or("unknown")) + << "hour:" << ::toQByteArrayLatin1(hourStr.value_or("unknown")); + + if (yearStr.has_value() && monthStr.has_value() && dayStr.has_value() && hourStr.has_value()) { + const int year = stoi(yearStr.value()); + const int day = stoi(dayStr.value()); + const int hour = stoi(hourStr.value()); + emit sig_sendGameTimeToClock(year, monthStr.value(), day, hour); + } +} diff --git a/src/proxy/MudTelnet.h b/src/proxy/MudTelnet.h index 87d09de2b..e3d9f5131 100644 --- a/src/proxy/MudTelnet.h +++ b/src/proxy/MudTelnet.h @@ -33,17 +33,24 @@ public slots: void sig_sendToSocket(const QByteArray &); void sig_relayEchoMode(bool); void sig_relayGmcp(const GmcpMessage &); + void sig_sendMSSPToUser(const QByteArray &); + void sig_sendGameTimeToClock(const int year, + const std::string &month, + const int day, + const int hour); private: void virt_sendToMapper(const QByteArray &data, bool goAhead) final; void virt_receiveEchoMode(bool toggle) final; void virt_receiveGmcpMessage(const GmcpMessage &) final; + void virt_receiveMudServerStatus(const QByteArray &) final; void virt_onGmcpEnabled() final; void virt_sendRawData(const std::string_view data) final; private: void receiveGmcpModule(const GmcpModule &, bool); void resetGmcpModules(); + void parseMudServerStatus(const QByteArray &); private: void sendCoreSupports(); diff --git a/src/proxy/UserTelnet.cpp b/src/proxy/UserTelnet.cpp index b9523d5cd..d975f9ea1 100644 --- a/src/proxy/UserTelnet.cpp +++ b/src/proxy/UserTelnet.cpp @@ -127,6 +127,11 @@ void UserTelnet::slot_onGmcpToUser(const GmcpMessage &msg) } } +void UserTelnet::slot_onSendMSSPToUser(const QByteArray &data) +{ + sendMudServerStatus(data); +} + void UserTelnet::virt_sendToMapper(const QByteArray &data, const bool goAhead) { // MMapper requires all data to be Latin-1 internally diff --git a/src/proxy/UserTelnet.h b/src/proxy/UserTelnet.h index 7b252214c..2cad02e24 100644 --- a/src/proxy/UserTelnet.h +++ b/src/proxy/UserTelnet.h @@ -31,6 +31,7 @@ public slots: void slot_onConnected(); void slot_onRelayEchoMode(bool); void slot_onGmcpToUser(const GmcpMessage &); + void slot_onSendMSSPToUser(const QByteArray &); signals: void sig_analyzeUserStream(const QByteArray &, bool goAhead); diff --git a/src/proxy/proxy.cpp b/src/proxy/proxy.cpp index f62cd1453..10a22fcf3 100644 --- a/src/proxy/proxy.cpp +++ b/src/proxy/proxy.cpp @@ -164,6 +164,15 @@ void Proxy::slot_start() connect(mudTelnet, &MudTelnet::sig_sendToSocket, this, &Proxy::slot_onSendToMudSocket); connect(mudTelnet, &MudTelnet::sig_relayEchoMode, userTelnet, &UserTelnet::slot_onRelayEchoMode); connect(mudTelnet, &MudTelnet::sig_relayGmcp, userTelnet, &UserTelnet::slot_onGmcpToUser); + connect(mudTelnet, + &MudTelnet::sig_sendGameTimeToClock, + this, + &Proxy::slot_onSendGameTimeToClock); + connect(mudTelnet, + &MudTelnet::sig_sendMSSPToUser, + userTelnet, + &UserTelnet::slot_onSendMSSPToUser); + connect(mudTelnet, &MudTelnet::sig_relayGmcp, &m_groupManager, @@ -565,3 +574,13 @@ bool Proxy::isGmcpModuleEnabled(const GmcpModuleTypeEnum &module) const { return m_userTelnet->isGmcpModuleEnabled(module); } + +void Proxy::slot_onSendGameTimeToClock(const int year, + const std::string &monthStr, + const int day, + const int hour) +{ + // Month from MSSP comes as a string, so fetch the month index. + const int month = m_mumeClock.getMumeMonth(::toQStringLatin1(monthStr)); + m_mumeClock.parseMSSP(year, month, day, hour); +} diff --git a/src/proxy/proxy.h b/src/proxy/proxy.h index 8f4ff5412..bd61e97d5 100644 --- a/src/proxy/proxy.h +++ b/src/proxy/proxy.h @@ -70,6 +70,11 @@ public slots: void slot_onMudError(const QString &); void slot_onMudConnected(); + void slot_onSendGameTimeToClock(const int year, + const std::string &month, + const int day, + const int hour); + signals: void sig_log(const QString &, const QString &); diff --git a/tests/testclock.cpp b/tests/testclock.cpp index 79088a19c..277f5a10f 100644 --- a/tests/testclock.cpp +++ b/tests/testclock.cpp @@ -111,6 +111,60 @@ void TestClock::parseMumeTimeTest() QCOMPARE(clock.toMumeTime(clock.getMumeMoment()), expected6); } +void TestClock::getMumeMonthTest() +{ + GameObserver observer; + MumeClock clock(observer); + + int month0 = clock.getMumeMonth("Narwain"); + int expected0 = 0; + QCOMPARE(month0, expected0); + + int month1 = clock.getMumeMonth("Solmath"); + int expected1 = 1; + QCOMPARE(month1, expected1); + + int month2 = clock.getMumeMonth("Gwaeron"); + int expected2 = 2; + QCOMPARE(month2, expected2); + + int month3 = clock.getMumeMonth("Astron"); + int expected3 = 3; + QCOMPARE(month3, expected3); + + int month4 = clock.getMumeMonth("Lothron"); + int expected4 = 4; + QCOMPARE(month4, expected4); + + int month5 = clock.getMumeMonth("Forelithe"); + int expected5 = 5; + QCOMPARE(month5, expected5); + + int month6 = clock.getMumeMonth("Cerveth"); + int expected6 = 6; + QCOMPARE(month6, expected6); + + int month7 = clock.getMumeMonth("Wedmath"); + int expected7 = 7; + QCOMPARE(month7, expected7); + + int month8 = clock.getMumeMonth("Ivanneth"); + int expected8 = 8; + QCOMPARE(month8, expected8); + + int month9 = clock.getMumeMonth("Winterfilth"); + int expected9 = 9; + QCOMPARE(month9, expected9); + + int month10 = clock.getMumeMonth("Hithui"); + int expected10 = 10; + QCOMPARE(month10, expected10); + + int month11 = clock.getMumeMonth("Foreyule"); + int expected11 = 11; + QCOMPARE(month11, expected11); +} + void TestClock::parseWeatherClockSkewTest() { GameObserver observer; diff --git a/tests/testclock.h b/tests/testclock.h index 733bbfefb..a6da82ed0 100644 --- a/tests/testclock.h +++ b/tests/testclock.h @@ -16,6 +16,7 @@ private Q_SLOTS: // MumeClock void mumeClockTest(); void parseMumeTimeTest(); + void getMumeMonthTest(); void parseWeatherTest(); void parseWeatherClockSkewTest(); void parseClockTimeTest();