Skip to content
Merged
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
3 changes: 3 additions & 0 deletions Source/RMG-Core/Settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,9 @@ static l_Setting get_setting(SettingsID settingId)
case SettingsID::GUI_OnScreenDisplayKailleraPortLabels:
setting = {SETTING_SECTION_GUI, "OnScreenDisplayKailleraPortLabels", false};
break;
case SettingsID::GUI_OnScreenDisplayLiveReplayStatus:
setting = {SETTING_SECTION_GUI, "OnScreenDisplayLiveReplayStatus", false};
break;
case SettingsID::GUI_AutoStartNetplayOnStartup:
setting = {SETTING_SECTION_GUI, "AutoStartNetplayOnStartup", false};
break;
Expand Down
1 change: 1 addition & 0 deletions Source/RMG-Core/Settings.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ enum class SettingsID
GUI_OnScreenDisplayMaxMessages,
GUI_OnScreenDisplayChatEnabled,
GUI_OnScreenDisplayKailleraPortLabels,
GUI_OnScreenDisplayLiveReplayStatus,
GUI_AutoStartNetplayOnStartup,
GUI_Toolbar,
GUI_ToolbarArea,
Expand Down
4 changes: 3 additions & 1 deletion Source/RMG/OnScreenDisplay.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ static constexpr std::array<float, 4> l_KailleraPortLabelCenterOffset720p = { 19
static std::string l_CenterMessage;
// Persistent audience badge for broadcasters and spectators. Unlike ordinary
// system messages, it remains visible until explicitly cleared.
static bool l_LiveReplayStatusEnabled = false;
static std::string l_LiveReplayStatus;
static std::mutex l_LiveReplayStatusMutex;

Expand Down Expand Up @@ -387,6 +388,7 @@ void OnScreenDisplayLoadSettings(void)
l_Enabled = CoreSettingsGetBoolValue(SettingsID::GUI_OnScreenDisplayEnabled);
l_KailleraChatEnabled = CoreSettingsGetBoolValue(SettingsID::GUI_OnScreenDisplayChatEnabled);
l_KailleraPortLabelsEnabled = CoreSettingsGetBoolValue(SettingsID::GUI_OnScreenDisplayKailleraPortLabels);
l_LiveReplayStatusEnabled = CoreSettingsGetBoolValue(SettingsID::GUI_OnScreenDisplayLiveReplayStatus);
l_MessagePosition = CoreSettingsGetIntValue(SettingsID::GUI_OnScreenDisplayLocation);
l_MessagePaddingX = CoreSettingsGetIntValue(SettingsID::GUI_OnScreenDisplayPaddingX);
l_MessagePaddingY = CoreSettingsGetIntValue(SettingsID::GUI_OnScreenDisplayPaddingY);
Expand Down Expand Up @@ -674,7 +676,7 @@ void OnScreenDisplayRender(void)
}
const bool hasPortLabels = l_KailleraPortLabelsEnabled && l_KailleraPortLabelPlayerCount > 0;
const bool hasCenterMessage = !l_CenterMessage.empty();
const bool hasLiveReplayStatus = !liveReplayStatus.empty();
const bool hasLiveReplayStatus = l_LiveReplayStatusEnabled && !liveReplayStatus.empty();
const bool hasMessages = l_Enabled && (hasVisibleQueueMessage || l_InputPromptActive ||
hasPortLabels || hasCenterMessage || hasLiveReplayStatus);

Expand Down
154 changes: 77 additions & 77 deletions Source/RMG/UserInterface/Dialog/Lobby/RollbackLobbyDialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1126,18 +1126,11 @@ QWidget* RollbackLobbyDialog::buildInRoomView()
m_roomTitle->setWordWrap(true);
lay->addWidget(m_roomTitle);

// Subtitle (host / max players) — default WindowText since this is
// important info; the bold title above gives the hierarchy.
m_roomSubtitle = new QLabel("—", this);
m_roomSubtitle->setWordWrap(true);
lay->addWidget(m_roomSubtitle);

// Meta line (Seats / Region) — plain rich text. Local delay lives in the
// editable settings row below instead of a read-only chip.
m_roomMetaLabel = new QLabel("—", this);
m_roomMetaLabel->setTextFormat(Qt::RichText);
m_roomMetaLabel->setContentsMargins(0, SPACING_TIGHT, 0, 0);
lay->addWidget(m_roomMetaLabel);
m_roomRegionLabel = new QLabel(this);
m_roomRegionLabel->setTextFormat(Qt::RichText);
m_roomRegionLabel->setContentsMargins(0, SPACING_TIGHT, 0, 0);
m_roomRegionLabel->setVisible(false);
lay->addWidget(m_roomRegionLabel);

// ── Rollback settings row ──
auto* settingsRow = new QHBoxLayout;
Expand Down Expand Up @@ -1207,14 +1200,9 @@ QWidget* RollbackLobbyDialog::buildInRoomView()
toggleRow->setContentsMargins(0, 0, 0, 0);
toggleRow->setSpacing(SPACING_DEFAULT);

// Per-player local recording toggle. Stays enabled for everyone — each
// player decides whether to save their own .krec. Initialized from the
// cap-aware default and kept in the shared n02 recording flag, exactly like
// the p2p / kaillera lobbies.
m_recordCheck = new QCheckBox("Record game", this);
m_recordCheck->setToolTip(
"Record this match to a .krec file on your PC.\n"
"Local setting — each player records their own copy.");
// Per-player local saving, independent of the host's Live Replay choice.
// Editable before the match and initialized from the cap-aware default.
m_recordCheck = new QCheckBox("Record locally", this);
const bool recordingDefault = CoreGetKailleraEffectiveRecordingDefault();
n02_kaillera_recording_enabled = recordingDefault;
m_recordCheck->setChecked(recordingDefault);
Expand All @@ -1223,23 +1211,17 @@ QWidget* RollbackLobbyDialog::buildInRoomView()
});
toggleRow->addWidget(m_recordCheck);

// Broadcast: stream this match's krec to the server so others can spectate.
// Broadcasting implies recording (the stream is the krec), so ticking it
// also forces "Record game" on.
// Broadcast replay bytes to the server without requiring a local file.
m_broadcastCheck = new QCheckBox("Live Replay", this);
m_broadcastCheck->setToolTip(
"Let others in the lobby watch this match live.\n"
"Implies Record game (the live replay is the .krec). Only one player\n"
"per match streams it — whoever enables it first.");
connect(m_broadcastCheck, &QCheckBox::toggled, this, [this](bool checked) {
if (m_suppressSettingsSignal) return;
if (checked && m_recordCheck)
m_recordCheck->setChecked(true); // broadcasting needs the krec written
updateRecordingControls();
if (m_currentRoomId != 0 && m_client)
m_client->updateRoomLiveReplay(checked);
});
toggleRow->addWidget(m_broadcastCheck);
toggleRow->addStretch(1);
updateRecordingControls();

lay->addLayout(toggleRow);

Expand All @@ -1265,10 +1247,17 @@ QWidget* RollbackLobbyDialog::buildInRoomView()
this, applyPrediction);

// ── Seats — bold section header + vertical player-list rows ──
auto* seatsHeaderRow = new QHBoxLayout;
seatsHeaderRow->setContentsMargins(0, SPACING_DEFAULT, 0, 0);
seatsHeaderRow->setSpacing(SPACING_TIGHT);
auto* seatsHeader = new QLabel("SEATS", this);
seatsHeader->setProperty("class", "SectionHeader");
seatsHeader->setContentsMargins(0, SPACING_DEFAULT, 0, 0);
lay->addWidget(seatsHeader);
seatsHeaderRow->addWidget(seatsHeader, 0, Qt::AlignBaseline);
m_seatCountLabel = new QLabel(this);
bumpFont(m_seatCountLabel, -1);
seatsHeaderRow->addWidget(m_seatCountLabel, 0, Qt::AlignBaseline);
seatsHeaderRow->addStretch(1);
lay->addLayout(seatsHeaderRow);

auto* seatsBox = new QWidget(this);
m_seatsBox = seatsBox;
Expand Down Expand Up @@ -2956,8 +2945,8 @@ void RollbackLobbyDialog::clearServerRoomSnapshot()
m_probeFailureAnnounced.clear();

if (m_roomTitle) m_roomTitle->setText(QStringLiteral("—"));
if (m_roomSubtitle) m_roomSubtitle->setText(QStringLiteral("—"));
if (m_roomMetaLabel) m_roomMetaLabel->setText(QStringLiteral("—"));
if (m_roomRegionLabel) m_roomRegionLabel->setVisible(false);
if (m_seatCountLabel) m_seatCountLabel->clear();
if (m_chatViewRoom) m_chatViewRoom->clear();
if (m_roomChatInput)
{
Expand Down Expand Up @@ -3361,29 +3350,12 @@ void RollbackLobbyDialog::onRoomStateChanged(const QJsonObject& roomState)
title = QString("%1 — %2").arg(name, romName);
m_roomTitle->setText(title);

// Resolve host display name: prefer hostName, fall back to users() map,
// then to our own username if we're the host.
QString hostName = roomState.value("hostName").toString();
if (hostName.isEmpty())
{
const auto& users = m_client->users();
const auto it = users.constFind(hostId);
if (it != users.constEnd()) hostName = it->username;
}
if (hostName.isEmpty() && iAmHost) hostName = m_username;
if (hostName.isEmpty()) hostName = QStringLiteral("—");

m_roomSubtitle->setText(QString("Hosted by %1 · %2 players max")
.arg(hostName).arg(maxPlayers));

applyRoomStateBadge(roomStateLabel(state), stateHex(state, isDarkTheme()));

const QJsonArray players = roomState.value("players").toArray();
QStringList metaParts;
metaParts << QString("<b>Seats:</b> %1/%2").arg(players.size()).arg(maxPlayers);
if (!romRegion.isEmpty())
metaParts << QString("<b>Region:</b> %1").arg(romRegion);
m_roomMetaLabel->setText(metaParts.join(" · "));
m_seatCountLabel->setText(QString("(%1/%2)").arg(players.size()).arg(maxPlayers));
m_roomRegionLabel->setText(QString("<b>Region:</b> %1").arg(romRegion.toHtmlEscaped()));
m_roomRegionLabel->setVisible(!romRegion.isEmpty());

// Delay remains local to every player. Prediction mirrors authoritative
// room state and is editable only by the host while waiting.
Expand Down Expand Up @@ -3420,32 +3392,15 @@ void RollbackLobbyDialog::onRoomStateChanged(const QJsonObject& roomState)

// The host owns the room-wide live-replay choice, but every seated player
// sees its authoritative value. Non-hosts get a disabled checkbox instead
// of a hidden option. "Record game" remains each player's local choice.
// of a hidden option. Local saving remains each player's separate choice.
if (m_broadcastCheck)
{
m_suppressSettingsSignal = true;
m_broadcastCheck->setChecked(liveReplayEnabled);
m_suppressSettingsSignal = false;
m_broadcastCheck->setVisible(true);
m_broadcastCheck->setEnabled(iAmHost && state == "waiting");
if (!iAmHost)
{
m_broadcastCheck->setToolTip(liveReplayEnabled
? QStringLiteral("The room host will stream this match as a live replay.")
: QStringLiteral("The room host has live replay streaming turned off."));
}
else if (state != "waiting")
{
m_broadcastCheck->setToolTip(
QStringLiteral("The live replay choice is locked while a match is running."));
}
else
{
m_broadcastCheck->setToolTip(
QStringLiteral("Let others in the lobby watch this match live.\n"
"Implies Record game (the live replay is the .krec)."));
}
}
updateRecordingControls();

// ── Seats ──
QVector<bool> filled(4, false);
Expand Down Expand Up @@ -3553,6 +3508,49 @@ void RollbackLobbyDialog::onRoomStateChanged(const QJsonObject& roomState)
refreshStartButton();
}

void RollbackLobbyDialog::updateRecordingControls()
{
if (!m_recordCheck || !m_broadcastCheck)
return;

const bool iAmHost = m_client && m_currentRoomId != 0 &&
m_currentRoomHostId == m_client->selfUserId();
const bool liveReplayEnabled = m_broadcastCheck->isChecked();
const bool editable = m_currentRoomState == "waiting" && !matchTransportInProgress();

m_recordCheck->setEnabled(editable);
if (!editable)
{
m_recordCheck->setToolTip(
QStringLiteral("Recording can't be changed during a match."));
}
else
{
m_recordCheck->setToolTip(
QStringLiteral("Record this match to a .krec file on your PC.\n"
"Independent of Live Replay — each player chooses whether to save a copy."));
}

m_broadcastCheck->setEnabled(iAmHost && editable);
if (!iAmHost)
{
m_broadcastCheck->setToolTip(liveReplayEnabled
? QStringLiteral("The room host will stream this match as a live replay.")
: QStringLiteral("The room host has live replay streaming turned off."));
}
else if (!editable)
{
m_broadcastCheck->setToolTip(
QStringLiteral("The live replay choice is locked while a match is running."));
}
else
{
m_broadcastCheck->setToolTip(
QStringLiteral("Let others in the lobby watch this match live.\n"
"A local replay is saved only if Record locally is checked."));
}
}

void RollbackLobbyDialog::refreshStartButton()
{
if (!m_startBtn)
Expand Down Expand Up @@ -3652,6 +3650,7 @@ void RollbackLobbyDialog::notifyEmulationFinished()
m_awaitingEmulationStart = false;
if (m_client)
m_client->setMatchTransportActive(false);
updateRecordingControls();

{
char buf[128];
Expand Down Expand Up @@ -4582,6 +4581,7 @@ void RollbackLobbyDialog::abortMatchStart(const QString& reason)
emit closeMatchRequested();
if (matchId != 0 && m_client)
m_client->reportMatchFinished(matchId);
updateRecordingControls();

if (m_dropBtn) m_dropBtn->setEnabled(false);

Expand Down Expand Up @@ -4649,6 +4649,7 @@ void RollbackLobbyDialog::onMatchBegin(quint64 matchId, const QList<LobbyClient:

m_currentMatchId = matchId;
m_awaitingEmulationStart = true;
updateRecordingControls();

// Disable start; enable Drop now — MATCH_BEGIN arrives *after* the
// ROOM_STATE that flipped us to in_game, so onRoomStateChanged ran before
Expand Down Expand Up @@ -4695,7 +4696,7 @@ void RollbackLobbyDialog::onMatchBegin(quint64 matchId, const QList<LobbyClient:
return;
}

// Make this room's "Record game" checkbox authoritative for the match, in
// Make this room's "Record locally" checkbox authoritative for the match, in
// case another netplay dialog touched the shared flag since the box was built.
if (m_recordCheck)
{
Expand All @@ -4704,20 +4705,19 @@ void RollbackLobbyDialog::onMatchBegin(quint64 matchId, const QList<LobbyClient:

// If broadcasting, arm the krec tee and announce to the server now — before
// MainWindow calls recordingOpen, so the .krec header is captured too. Host-
// only: the broadcast checkbox is hidden for non-hosts, and the server also
// only: the broadcast checkbox is disabled for non-hosts, and the server also
// rejects a non-host BROADCAST_BEGIN — this is the matching client guard.
const bool iAmHostForBroadcast =
(m_client && m_currentRoomHostId == m_client->selfUserId());
if (iAmHostForBroadcast && m_broadcastCheck && m_broadcastCheck->isChecked())
{
n02_kaillera_recording_enabled = true; // broadcasting requires the krec
startBroadcast(matchId);
}

// Capture seated player names (slot-indexed) for the .krec header, the same
// way the p2p / kaillera paths fill recording_player_names before a recording
// opens. MainWindow opens the file when it launches the match; this is
// harmless when "Record game" is off (the open self-gates on the flag).
// starts. MainWindow begins replay capture when it launches the match,
// saving locally and/or streaming according to the two independent choices.
{
std::memset(recording_player_names, 0, sizeof(recording_player_names));
for (const auto& p : peers)
Expand Down
13 changes: 6 additions & 7 deletions Source/RMG/UserInterface/Dialog/Lobby/RollbackLobbyDialog.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@ private slots:
// both onRoomStateChanged (seats change) and onPingMeasured (ping lands),
// since pings arrive asynchronously after seats populate.
void refreshStartButton();
void updateRecordingControls();

LobbyClient* m_client = nullptr;

Expand Down Expand Up @@ -374,9 +375,9 @@ private slots:

// In-room header card
QLabel* m_roomTitle = nullptr; // ROM title (large)
QLabel* m_roomSubtitle = nullptr; // host · region · max
QLabel* m_roomStateLabel = nullptr; // "Waiting" / "In Game"
QLabel* m_roomMetaLabel = nullptr; // Seats 2/4 · Region NTSC
QLabel* m_roomRegionLabel = nullptr; // Region NTSC, when available
QLabel* m_seatCountLabel = nullptr; // (2/4) beside SEATS

// Delay is locally editable for every player; prediction is room-wide and
// host-editable. Both lock once the match starts. Delay uses data -1 for
Expand All @@ -390,11 +391,9 @@ private slots:
// Drives the shared n02_kaillera_recording_enabled flag; not synced to the room.
QCheckBox* m_recordCheck = nullptr;

// When checked, this client also streams the match's .krec up to the server
// so others can spectate. Broadcasting implies recording (the stream is the
// krec bytes). Only one player per match becomes the broadcaster (server
// picks the first). The krec is written on the emulation thread, so bytes
// are staged into m_broadcastBuf (under m_broadcastMutex) by the n02 sink
// When checked, the room host streams replay bytes so others can spectate,
// independently of whether a local .krec file is saved. On the emulation thread,
// bytes are staged into m_broadcastBuf (under m_broadcastMutex) by the n02 sink
// and drained to the WebSocket by m_broadcastDrainTimer on the UI thread.
QCheckBox* m_broadcastCheck = nullptr;
QTimer* m_broadcastDrainTimer = nullptr;
Expand Down
4 changes: 4 additions & 0 deletions Source/RMG/UserInterface/Dialog/SettingsDialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -977,6 +977,7 @@ void SettingsDialog::loadInterfaceOSDSettings(void)
this->osdScaleDoubleSpinBox->setValue(CoreSettingsGetFloatValue(SettingsID::GUI_OnScreenDisplayScale));
this->osdMaxMessagesSpinBox->setValue(CoreSettingsGetIntValue(SettingsID::GUI_OnScreenDisplayMaxMessages));
this->osdKailleraPortLabelsCheckBox->setChecked(CoreSettingsGetBoolValue(SettingsID::GUI_OnScreenDisplayKailleraPortLabels));
this->osdLiveReplayStatusCheckBox->setChecked(CoreSettingsGetBoolValue(SettingsID::GUI_OnScreenDisplayLiveReplayStatus));

std::vector<int> backgroundColor = CoreSettingsGetIntListValue(SettingsID::GUI_OnScreenDisplayBackgroundColor);
std::vector<int> textColor = CoreSettingsGetIntListValue(SettingsID::GUI_OnScreenDisplayTextColor);
Expand Down Expand Up @@ -1207,6 +1208,7 @@ void SettingsDialog::loadDefaultInterfaceOSDSettings(void)
this->osdScaleDoubleSpinBox->setValue(CoreSettingsGetDefaultFloatValue(SettingsID::GUI_OnScreenDisplayScale));
this->osdMaxMessagesSpinBox->setValue(CoreSettingsGetDefaultIntValue(SettingsID::GUI_OnScreenDisplayMaxMessages));
this->osdKailleraPortLabelsCheckBox->setChecked(CoreSettingsGetDefaultBoolValue(SettingsID::GUI_OnScreenDisplayKailleraPortLabels));
this->osdLiveReplayStatusCheckBox->setChecked(CoreSettingsGetDefaultBoolValue(SettingsID::GUI_OnScreenDisplayLiveReplayStatus));

const std::vector<int> backgroundColor = CoreSettingsGetDefaultIntListValue(SettingsID::GUI_OnScreenDisplayBackgroundColor);
const std::vector<int> textColor = CoreSettingsGetDefaultIntListValue(SettingsID::GUI_OnScreenDisplayTextColor);
Expand Down Expand Up @@ -1492,6 +1494,7 @@ void SettingsDialog::saveInterfaceOSDSettings(void)
CoreSettingsSetValue(SettingsID::GUI_OnScreenDisplayScale, static_cast<float>(this->osdScaleDoubleSpinBox->value()));
CoreSettingsSetValue(SettingsID::GUI_OnScreenDisplayMaxMessages, this->osdMaxMessagesSpinBox->value());
CoreSettingsSetValue(SettingsID::GUI_OnScreenDisplayKailleraPortLabels, this->osdKailleraPortLabelsCheckBox->isChecked());
CoreSettingsSetValue(SettingsID::GUI_OnScreenDisplayLiveReplayStatus, this->osdLiveReplayStatusCheckBox->isChecked());
CoreSettingsSetValue(SettingsID::GUI_OnScreenDisplayBackgroundColor, std::vector<int>({ this->currentBackgroundColor.red(),
this->currentBackgroundColor.green(),
this->currentBackgroundColor.blue(),
Expand Down Expand Up @@ -1843,6 +1846,7 @@ void SettingsDialog::updateOSDSettingsEnabledState(void)
this->osdChatEnabledCheckBox->setEnabled(osdEnabled);
this->osdChatSettingsGroupBox->setEnabled(osdEnabled && chatEnabled);
this->osdKailleraPortLabelsCheckBox->setEnabled(osdEnabled);
this->osdLiveReplayStatusCheckBox->setEnabled(osdEnabled);
}

void SettingsDialog::chooseDirectory(QLineEdit *lineEdit, QString caption)
Expand Down
Loading
Loading