Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/configuration/configuration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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<qlonglong>(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
Expand Down
2 changes: 2 additions & 0 deletions src/configuration/configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,8 @@ class NODISCARD Configuration final
{
int64_t startEpoch = 0;
bool display = false;
bool gmcpBroadcast = true;
int gmcpBroadcastInterval = 2500;

private:
SUBGROUP();
Expand Down
1 change: 1 addition & 0 deletions src/opengl/OpenGLProber.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include <QOpenGLContext>

#ifdef WIN32
#include <windows.h>
extern "C" {
// Prefer discrete nVidia and AMD GPUs by default on Windows
__declspec(dllexport) DWORD NvOptimusEnablement = 0x00000001;
Expand Down
25 changes: 25 additions & 0 deletions src/preferences/mumeprotocolpage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>::of(&QSpinBox::valueChanged),
this,
&MumeProtocolPage::slot_gmcpIntervalSpinBoxChanged);
}

MumeProtocolPage::~MumeProtocolPage()
Expand All @@ -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*/)
Expand Down Expand Up @@ -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;
}
2 changes: 2 additions & 0 deletions src/preferences/mumeprotocolpage.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
45 changes: 45 additions & 0 deletions src/preferences/mumeprotocolpage.ui
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,51 @@
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="gmcpClockGroupBox">
<property name="title">
<string>GMCP Clock Broadcasting</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0" colspan="2">
<widget class="QCheckBox" name="gmcpBroadcastCheckBox">
<property name="text">
<string>Broadcast clock to connected clients (GMCP MUME.Time)</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="gmcpIntervalLabel">
<property name="text">
<string>Update interval (ms):</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="gmcpIntervalSpinBox">
<property name="toolTip">
<string>How often to send clock updates to clients (default: 2500ms = 1 MUME minute)</string>
</property>
<property name="minimum">
<number>500</number>
</property>
<property name="maximum">
<number>60000</number>
</property>
<property name="singleStep">
<number>500</number>
</property>
<property name="value">
<number>2500</number>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer>
<property name="orientation">
Expand Down
3 changes: 2 additions & 1 deletion src/proxy/GmcpMessage.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
3 changes: 2 additions & 1 deletion src/proxy/GmcpModule.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions src/proxy/UserTelnet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/proxy/UserTelnet.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,18 @@ 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;
virtual void virt_onSendToSocket(const TelnetIacBytes &) = 0;
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
Expand Down
152 changes: 152 additions & 0 deletions src/proxy/proxy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,14 @@
#include <tuple>

#include <QByteArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QMessageLogContext>
#include <QObject>
#include <QScopedPointer>
#include <QSslSocket>
#include <QTcpSocket>
#include <QTimer>

using mmqt::makeQPointer;

Expand Down Expand Up @@ -177,6 +180,8 @@ Proxy::~Proxy()
sendNewlineToUser();
sendStatusToUser("MMapper proxy is shutting down.");

stopClockBroadcaster();

{
qDebug() << "disconnecting mud socket...";
getMudSocket().disconnectFromHost();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -884,6 +899,143 @@ 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;
}

if (m_clockBroadcastTimer == nullptr) {
m_clockBroadcastTimer = new QTimer(this);
QObject::connect(m_clockBroadcastTimer, &QTimer::timeout, this, &Proxy::broadcastClockInfo);
}

// 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
broadcastClockInfo();
}

void Proxy::stopClockBroadcaster()
{
if (m_clockBroadcastTimer != nullptr && m_clockBroadcastTimer->isActive()) {
m_clockBroadcastTimer->stop();
}
}

void Proxy::broadcastClockInfo()
{
if (!getConfig().mumeClock.gmcpBroadcast
|| !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;

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;
json["duskHour"] = duskHour;
json["syncEpoch"] = static_cast<qint64>(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.
Expand Down
Loading