skate3recomp-multiplayer/src/skate3_native_ghost_dialog.cpp
MiTHRAL 4ec504ac6c ghost: fix auto-assigned room code flickering + copy button
When the relay auto-assigns a room, the code lives in the connection state
while the skate3_ghost_room cvar stays empty. The network loop was
overwriting the active room with the empty cvar every 25 ms, making the code
flicker in and out of the menu, and the Copy button copied the empty cvar.

- Loop: only sync the cvar into the active room when it is non-empty;
  preserve the server-assigned code otherwise.
- Menu: Copy grabs the ACTIVE room from GetStatus() (works for auto-assigned
  rooms, with a brief 'copied' hint); the room field now surfaces the
  auto-assigned code instead of staying blank.
2026-08-27 12:56:22 -04:00

287 lines
No EOL
9 KiB
C++

#include "skate3_native_ghost_dialog.h"
#include <cmath>
#include <cstring>
#include <string>
#include <imgui.h>
#include <rex/cvar.h>
#include <rex/logging.h>
#include "native/skate3_native_ghost.h"
#include "skate3_native_scene.h"
REXCVAR_DECLARE(bool, skate3_ghost_enable);
REXCVAR_DECLARE(std::string, skate3_ghost_server);
REXCVAR_DECLARE(int32_t, skate3_ghost_port);
REXCVAR_DECLARE(std::string, skate3_ghost_room);
REXCVAR_DECLARE(std::string, skate3_ghost_name);
REXCVAR_DECLARE(double, skate3_ghost_rate);
REXCVAR_DECLARE(bool, skate3_ghost_draw_local);
namespace skate3 {
void GhostDialog::Show() {
visible_ = true;
SetDrawActive(true);
}
void GhostDialog::Hide() {
if (!visible_) {
return;
}
visible_ = false;
SetDrawActive(false);
}
void GhostDialog::Toggle() {
if (visible_) {
Hide();
} else {
Show();
}
}
void GhostDialog::SyncBuffersFromCvars() {
const std::string room = REXCVAR_GET(skate3_ghost_room);
if (!room.empty()) {
std::snprintf(room_input_, sizeof(room_input_), "%s", room.c_str());
} else {
// Auto-assigned room: the active code comes from the connection, not the
// (empty) cvar - surface it in the input so the user can see/copy it.
skate3::native_ghost::Status st;
if (skate3::native_ghost::GetStatus(&st) && st.room_len > 0) {
std::snprintf(room_input_, sizeof(room_input_), "%.*s",
int(st.room_len), st.room);
} else {
room_input_[0] = '\0';
}
}
const std::string server = REXCVAR_GET(skate3_ghost_server);
std::snprintf(server_input_, sizeof(server_input_), "%s", server.c_str());
const std::string name = REXCVAR_GET(skate3_ghost_name);
std::snprintf(name_input_, sizeof(name_input_), "%s", name.c_str());
synced_ = true;
}
void GhostDialog::OnDraw(ImGuiIO& io) {
(void)io;
if (!visible_) {
return;
}
if (!synced_) {
SyncBuffersFromCvars();
}
bool open = visible_;
ImGui::SetNextWindowSize(ImVec2(380.0f, 0.0f), ImGuiCond_FirstUseEver);
if (!ImGui::Begin("Ghost Multiplayer (M)", &open,
ImGuiWindowFlags_NoCollapse)) {
ImGui::End();
if (!open) {
Hide();
}
return;
}
bool enable = REXCVAR_GET(skate3_ghost_enable);
if (ImGui::Checkbox("Enable ghost multiplayer", &enable)) {
REXCVAR_SET(skate3_ghost_enable, enable);
if (enable) {
REXLOG_INFO("ghost: multiplayer enabled");
}
}
ImGui::SameLine();
ImGui::TextDisabled("(relays your position, draws remote players)");
ImGui::Separator();
ImGui::Text("Room code");
if (ImGui::InputText("##ghost_room", room_input_, sizeof(room_input_),
ImGuiInputTextFlags_EnterReturnsTrue |
ImGuiInputTextFlags_CharsUppercase)) {
REXCVAR_SET(skate3_ghost_room, std::string(room_input_));
ImGui::SetKeyboardFocusHere(-1);
}
ImGui::SameLine();
// Copy the ACTIVE room (the one this player is actually in), which for an
// auto-assigned room lives in the connection state, not the cvar.
if (ImGui::Button("Copy")) {
skate3::native_ghost::Status st;
if (skate3::native_ghost::GetStatus(&st) && st.room_len > 0) {
char buf[skate3::native_ghost::kGhostMaxRoomLen + 1] = {};
std::memcpy(buf, st.room, st.room_len);
ImGui::SetClipboardText(buf);
copy_feedback_ = true;
}
}
if (copy_feedback_) {
ImGui::SameLine();
ImGui::TextDisabled("copied");
copy_feedback_ = false;
}
ImGui::TextDisabled("Empty = the server assigns one; share it with friends.");
ImGui::Separator();
ImGui::Text("Display name");
if (ImGui::InputText("##ghost_name", name_input_, sizeof(name_input_),
ImGuiInputTextFlags_EnterReturnsTrue)) {
REXCVAR_SET(skate3_ghost_name, std::string(name_input_));
ImGui::SetKeyboardFocusHere(-1);
}
ImGui::TextDisabled("Shown on your nameplate above the ghost. Empty = "
"your OS user name.");
ImGui::Separator();
if (ImGui::CollapsingHeader("Browse rooms (lobby)", ImGuiTreeNodeFlags_DefaultOpen)) {
if (ImGui::Button("Refresh rooms")) {
skate3::native_ghost::RequestRooms();
}
ImGui::SameLine();
skate3::native_ghost::RoomList rl;
const bool have = skate3::native_ghost::GetRooms(&rl);
ImGui::TextDisabled(have ? "%u room(s) on the relay" : "no room list yet");
if (have) {
for (uint8_t i = 0; i < rl.count; ++i) {
const skate3::native_ghost::RoomEntry& e = rl.rooms[i];
std::string label(e.room, e.room_len);
label += " (" + std::to_string(int(e.member_count)) + " player" +
(e.member_count == 1 ? "" : "s") + ")";
std::string names;
for (uint8_t n = 0; n < e.shown_members; ++n) {
if (n != 0) {
names += ", ";
}
names += std::string(e.member_names[n], e.member_name_len[n]);
}
if (!names.empty()) {
label += " [" + names + "]";
}
const std::string code(e.room, e.room_len);
if (ImGui::Selectable(label.c_str(), false)) {
REXCVAR_SET(skate3_ghost_room, code);
std::snprintf(room_input_, sizeof(room_input_), "%s", code.c_str());
}
}
ImGui::TextDisabled("Click a room to join it (applies on the next "
"heartbeat).");
}
}
ImGui::Separator();
ImGui::Text("Relay server (optional)");
ImGui::PushItemWidth(-1.0f);
if (ImGui::InputText("##ghost_server", server_input_, sizeof(server_input_),
ImGuiInputTextFlags_EnterReturnsTrue)) {
REXCVAR_SET(skate3_ghost_server, std::string(server_input_));
ImGui::SetKeyboardFocusHere(-1);
}
ImGui::PopItemWidth();
ImGui::TextDisabled("Empty = built-in default relay. No port forwarding "
"needed on any client.");
ImGui::Separator();
int port = int(REXCVAR_GET(skate3_ghost_port));
if (ImGui::SliderInt("Port", &port, 1024, 65535)) {
REXCVAR_SET(skate3_ghost_port, int32_t(port));
}
const double rate = REXCVAR_GET(skate3_ghost_rate);
rate_int_ = int(rate + 0.5);
if (ImGui::SliderInt("Broadcast rate (Hz)", &rate_int_, 5, 60)) {
REXCVAR_SET(skate3_ghost_rate, double(rate_int_));
}
bool draw_local = REXCVAR_GET(skate3_ghost_draw_local);
if (ImGui::Checkbox("Show my own cube (self-check)", &draw_local)) {
REXCVAR_SET(skate3_ghost_draw_local, draw_local);
}
ImGui::Separator();
skate3::native_ghost::Status st;
skate3::native_ghost::GetStatus(&st);
ImGui::Text("Status:");
ImGui::Text(" Enabled: %s", st.enabled ? "yes" : "no");
ImGui::Text(" Connected: %s%s", st.connected ? "yes" : "no",
st.hosting ? " (LAN host)" : "");
ImGui::Text(" Session: %u", st.session_id);
ImGui::Text(" Room: %.*s", int(st.room_len), st.room);
ImGui::Text(" In room: %zu player(s)", st.roster + 1);
ImGui::Text(" Ghosts drawn: %zu", st.ghosts);
if (st.peer[0] != '\0') {
ImGui::Text(" Relay: %s", st.peer);
}
if (st.refused) {
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f),
" Refused by relay (protocol version mismatch)");
}
if (st.last_error[0] != '\0') {
ImGui::TextWrapped(" %s", st.last_error);
}
ImGui::End();
if (!open) {
Hide();
}
}
void GhostNameplateOverlay::OnDraw(ImGuiIO& io) {
if (!REXCVAR_GET(skate3_ghost_enable)) {
return;
}
if (io.DisplaySize.x < 16.0f || io.DisplaySize.y < 16.0f) {
return;
}
float vp[16];
float cam[3];
if (!skate3::native_scene::GetPublishedCamera(vp, cam)) {
return;
}
const uint32_t count = skate3::native_ghost::GhostCount();
if (count == 0) {
return;
}
ImDrawList* dl = ImGui::GetBackgroundDrawList();
const float w = io.DisplaySize.x;
const float h = io.DisplaySize.y;
for (uint32_t i = 0; i < count; ++i) {
float pos[3];
float color[3];
char name[32];
if (!skate3::native_ghost::GetGhostLabel(i, pos, color, name,
sizeof(name))) {
continue;
}
// Project the point ~1.75 m above the entity origin (roughly the head).
const float p[3] = {pos[0], pos[1] + 1.75f, pos[2]};
float c[4];
for (int r = 0; r < 4; ++r) {
c[r] = p[0] * vp[r] + p[1] * vp[4 + r] + p[2] * vp[8 + r] + vp[12 + r];
}
if (c[3] < 0.01f) {
continue; // behind the camera
}
const float ndc_x = c[0] / c[3];
const float ndc_y = c[1] / c[3];
if (ndc_x < -1.6f || ndc_x > 1.6f || ndc_y < -1.6f || ndc_y > 1.6f) {
continue;
}
const float sx = (ndc_x * 0.5f + 0.5f) * w;
const float sy = (1.0f - (ndc_y * 0.5f + 0.5f)) * h;
const ImU32 text_col =
IM_COL32(int(color[0] * 255.0f), int(color[1] * 255.0f),
int(color[2] * 255.0f), 235);
const ImVec2 text_size = ImGui::CalcTextSize(name);
// Small dark backdrop so the label stays readable over the world.
dl->AddRectFilled(ImVec2(sx - 4.0f, sy - 14.0f),
ImVec2(sx + text_size.x + 4.0f, sy + 2.0f),
IM_COL32(0, 0, 0, 140));
dl->AddText(ImVec2(sx, sy - 14.0f), text_col, name);
}
}
} // namespace skate3