Skip to content

Commit a2a12cd

Browse files
committed
feat: server list shows usernames from autosong.ninjam.com
- Switch default URL to autosong.ninjam.com/serverlist.php - Parse plain-text SERVER format with usernames - Show BPM/BPI, users/max, and 'Who's There' column - Tooltip shows full user list on hover - Auto-detect format (JSON fallback still works)
1 parent 93fa598 commit a2a12cd

File tree

6 files changed

+161
-15
lines changed

6 files changed

+161
-15
lines changed

src/build_number.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
#pragma once
2-
#define JAMWIDE_BUILD_NUMBER 104
2+
#define JAMWIDE_BUILD_NUMBER 105

src/net/server_list.cpp

Lines changed: 115 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
#include "wdl/jsonparse.h"
88

99
#include <cstdlib>
10+
#include <cstring>
11+
#include <vector>
1012

1113
namespace jamwide {
1214

@@ -89,8 +91,8 @@ void ServerListFetcher::request(const std::string& url) {
8991
if (url_.empty()) {
9092
return;
9193
}
92-
http_.addheader("User-Agent: NINJAM-CLAP");
93-
http_.addheader("Accept: application/json");
94+
http_.addheader("User-Agent: JamWide");
95+
http_.addheader("Accept: text/plain, application/json");
9496
http_.connect(url_.c_str());
9597
active_ = true;
9698
}
@@ -152,6 +154,117 @@ bool ServerListFetcher::poll(ServerListResult& result) {
152154

153155
bool ServerListFetcher::parse_response(const std::string& data,
154156
ServerListResult& result) {
157+
// Auto-detect format: plain text starts with "SERVER", JSON starts with '{' or '['
158+
if (!data.empty()) {
159+
size_t start = 0;
160+
while (start < data.size() && (data[start] == ' ' || data[start] == '\n' || data[start] == '\r')) {
161+
++start;
162+
}
163+
if (start < data.size() && data.substr(start, 6) == "SERVER") {
164+
return parse_ninjam_format(data, result);
165+
}
166+
}
167+
return parse_json_format(data, result);
168+
}
169+
170+
// Parse ninjam.com plain-text format:
171+
// SERVER "host:port" "BPM/BPI" "users/max:name1,name2,..."
172+
bool ServerListFetcher::parse_ninjam_format(const std::string& data,
173+
ServerListResult& result) {
174+
result.servers.clear();
175+
result.error.clear();
176+
177+
// Helper to extract quoted string
178+
auto extract_quoted = [](const char*& p) -> std::string {
179+
while (*p && *p != '"') ++p;
180+
if (*p != '"') return "";
181+
++p; // skip opening quote
182+
const char* start = p;
183+
while (*p && *p != '"') ++p;
184+
std::string s(start, p);
185+
if (*p == '"') ++p; // skip closing quote
186+
return s;
187+
};
188+
189+
const char* p = data.c_str();
190+
while (*p) {
191+
// Find start of line
192+
while (*p && (*p == ' ' || *p == '\t')) ++p;
193+
194+
// Check for SERVER keyword
195+
if (std::strncmp(p, "SERVER", 6) == 0) {
196+
p += 6;
197+
198+
// Extract 3 quoted strings
199+
std::string host_port = extract_quoted(p);
200+
std::string tempo = extract_quoted(p);
201+
std::string users_info = extract_quoted(p);
202+
203+
if (!host_port.empty()) {
204+
ServerListEntry entry;
205+
206+
// Parse host:port
207+
size_t colon = host_port.rfind(':');
208+
if (colon != std::string::npos) {
209+
entry.host = host_port.substr(0, colon);
210+
entry.port = parse_int(host_port.c_str() + colon + 1, 2049);
211+
} else {
212+
entry.host = host_port;
213+
entry.port = 2049;
214+
}
215+
entry.name = entry.host;
216+
217+
// Parse tempo: "BPM/BPI" or "lobby"
218+
if (tempo == "lobby" || tempo == "Lobby") {
219+
entry.is_lobby = true;
220+
entry.bpm = 0;
221+
entry.bpi = 0;
222+
} else {
223+
size_t slash = tempo.find('/');
224+
if (slash != std::string::npos) {
225+
// Format could be "110 BPM/16" or "110/16"
226+
std::string bpm_part = tempo.substr(0, slash);
227+
// Remove " BPM" suffix if present
228+
size_t bpm_pos = bpm_part.find(" BPM");
229+
if (bpm_pos != std::string::npos) {
230+
bpm_part = bpm_part.substr(0, bpm_pos);
231+
}
232+
entry.bpm = parse_int(bpm_part.c_str(), 0);
233+
entry.bpi = parse_int(tempo.c_str() + slash + 1, 0);
234+
}
235+
}
236+
237+
// Parse users: "current/max:name1,name2,..."
238+
size_t user_colon = users_info.find(':');
239+
if (user_colon != std::string::npos) {
240+
std::string counts = users_info.substr(0, user_colon);
241+
entry.user_list = users_info.substr(user_colon + 1);
242+
243+
// Remove "(empty)" placeholder
244+
if (entry.user_list == "(empty)") {
245+
entry.user_list.clear();
246+
}
247+
248+
size_t slash = counts.find('/');
249+
if (slash != std::string::npos) {
250+
entry.users = parse_int(counts.c_str(), 0);
251+
entry.max_users = parse_int(counts.c_str() + slash + 1, 0);
252+
}
253+
}
254+
255+
result.servers.push_back(std::move(entry));
256+
}
257+
}
258+
// Skip to next line
259+
while (*p && *p != '\n') ++p;
260+
if (*p == '\n') ++p;
261+
}
262+
263+
return true;
264+
}
265+
266+
bool ServerListFetcher::parse_json_format(const std::string& data,
267+
ServerListResult& result) {
155268
wdl_json_parser parser;
156269
wdl_json_element* root = parser.parse(data.c_str(),
157270
static_cast<int>(data.size()));

src/net/server_list.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ class ServerListFetcher {
2929
private:
3030
void reset_state();
3131
bool parse_response(const std::string& data, ServerListResult& result);
32+
bool parse_ninjam_format(const std::string& data, ServerListResult& result);
33+
bool parse_json_format(const std::string& data, ServerListResult& result);
3234

3335
JNL_HTTPGet http_;
3436
bool active_ = false;

src/ui/server_list_types.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@ struct ServerListEntry {
1313
std::string host;
1414
int port = 0;
1515
int users = 0;
16+
int max_users = 0; // max user slots
17+
std::string user_list; // comma-separated usernames
1618
std::string topic;
19+
int bpm = 0; // parsed BPM (0 for lobby)
20+
int bpi = 0; // parsed BPI (0 for lobby)
21+
bool is_lobby = false; // lobby flag
1722
};
1823

1924
#endif // SERVER_LIST_TYPES_H

src/ui/ui_server_browser.cpp

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -70,33 +70,59 @@ void ui_render_server_browser(jamwide::JamWidePlugin* plugin) {
7070
ImGuiTableFlags_RowBg |
7171
ImGuiTableFlags_BordersInnerH |
7272
ImGuiTableFlags_Resizable)) {
73-
ImGui::TableSetupColumn("Name");
74-
ImGui::TableSetupColumn("Address");
73+
ImGui::TableSetupColumn("Server");
74+
ImGui::TableSetupColumn("Tempo");
7575
ImGui::TableSetupColumn("Users");
76-
ImGui::TableSetupColumn("Topic");
77-
ImGui::TableSetupColumn("Action");
76+
ImGui::TableSetupColumn("Who's There");
77+
ImGui::TableSetupColumn("##Action", ImGuiTableColumnFlags_WidthFixed, 40.0f);
7878
ImGui::TableHeadersRow();
7979

8080
int idx = 0;
8181
for (const auto& entry : state.server_list) {
8282
ImGui::TableNextRow();
8383

84+
// Server name/address
8485
ImGui::TableSetColumnIndex(0);
85-
ImGui::TextUnformatted(entry.name.empty()
86-
? entry.host.c_str()
87-
: entry.name.c_str());
88-
89-
ImGui::TableSetColumnIndex(1);
9086
char addr[256];
9187
format_server_address(entry, addr, sizeof(addr));
9288
ImGui::TextUnformatted(addr);
9389

90+
// Tempo (BPM/BPI or Lobby)
91+
ImGui::TableSetColumnIndex(1);
92+
if (entry.is_lobby) {
93+
ImGui::TextDisabled("Lobby");
94+
} else if (entry.bpm > 0) {
95+
ImGui::Text("%d/%d", entry.bpm, entry.bpi);
96+
} else {
97+
ImGui::TextDisabled("-");
98+
}
99+
100+
// Users (current/max)
94101
ImGui::TableSetColumnIndex(2);
95-
ImGui::Text("%d", entry.users);
102+
if (entry.max_users > 0) {
103+
ImGui::Text("%d/%d", entry.users, entry.max_users);
104+
} else {
105+
ImGui::Text("%d", entry.users);
106+
}
96107

108+
// Usernames - truncated with tooltip
97109
ImGui::TableSetColumnIndex(3);
98-
ImGui::TextUnformatted(entry.topic.c_str());
110+
if (!entry.user_list.empty()) {
111+
// Truncate for display
112+
std::string display = entry.user_list;
113+
if (display.length() > 30) {
114+
display = display.substr(0, 27) + "...";
115+
}
116+
ImGui::TextUnformatted(display.c_str());
117+
// Tooltip with full list
118+
if (ImGui::IsItemHovered() && entry.user_list.length() > 30) {
119+
ImGui::SetTooltip("%s", entry.user_list.c_str());
120+
}
121+
} else {
122+
ImGui::TextDisabled("(empty)");
123+
}
99124

125+
// Use button
100126
ImGui::TableSetColumnIndex(4);
101127
ImGui::PushID(idx++);
102128
if (ImGui::SmallButton("Use")) {

src/ui/ui_state.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ struct UiState {
9898
std::string license_text;
9999

100100
// Public server list
101-
char server_list_url[256] = "http://ninbot.com/serverlist";
101+
char server_list_url[256] = "http://autosong.ninjam.com/serverlist.php";
102102
std::vector<ServerListEntry> server_list;
103103
bool server_list_loading = false;
104104
std::string server_list_error;

0 commit comments

Comments
 (0)