Skip to content

Commit dcd666b

Browse files
committed
Added Host UI for adding player usernames to a roster, implemented HTTP request to Mojang API to fetch UUIDs, added display names and roster persistence for settings.json.
1 parent ce8291e commit dcd666b

6 files changed

Lines changed: 318 additions & 1 deletion

File tree

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ set(SOURCE_FILES
6262
"source/temp_creator.cpp"
6363
"source/temp_creator_utils.cpp"
6464
"source/template_scanner.cpp"
65+
"source/mojang_api.cpp"
6566
"source/update_checker.cpp"
6667
"source/logger.cpp"
6768
"source/dialog_utils.cpp"

source/mojang_api.cpp

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Copyright (c) 2026 LNXSeus. All Rights Reserved.
2+
//
3+
// This project is proprietary software. You are granted a license to use the software as-is.
4+
// You may not copy, distribute, modify, reverse-engineer, maintain a fork, or use this software
5+
// or its source code in any way without the express written permission of the copyright holder.
6+
//
7+
// Created by Linus on 06.04.2026.
8+
//
9+
10+
#include "mojang_api.h"
11+
#include "logger.h"
12+
#include <cJSON.h>
13+
#include <curl/curl.h>
14+
#include <string>
15+
#include <cstring>
16+
#include <cstdio>
17+
18+
// Path to the CA certificate bundle (same as update_checker.cpp)
19+
#define CERT_BUNDLE_PATH "resources/ca_certificates/cacert.pem"
20+
21+
// Callback function for libcurl to write received data into a std::string
22+
static size_t mojang_write_callback(void *contents, size_t size, size_t nmemb, std::string *s) {
23+
size_t new_length = size * nmemb;
24+
try {
25+
s->append((char *) contents, new_length);
26+
} catch (std::bad_alloc &) {
27+
return 0;
28+
}
29+
return new_length;
30+
}
31+
32+
// Formats a raw 32-char UUID into the hyphenated form: 8-4-4-4-12
33+
static void format_uuid_with_hyphens(const char *raw, char *out, size_t out_len) {
34+
snprintf(out, out_len, "%.8s-%.4s-%.4s-%.4s-%.12s",
35+
raw, raw + 8, raw + 12, raw + 16, raw + 20);
36+
}
37+
38+
bool mojang_fetch_uuid(const char *username, char *out_uuid, size_t uuid_max_len) {
39+
if (!username || username[0] == '\0' || !out_uuid || uuid_max_len < 37) {
40+
return false;
41+
}
42+
out_uuid[0] = '\0';
43+
44+
// Build the API URL
45+
char url[256];
46+
snprintf(url, sizeof(url), "https://api.mojang.com/users/profiles/minecraft/%s", username);
47+
48+
CURL *curl = curl_easy_init();
49+
if (!curl) {
50+
log_message(LOG_ERROR, "[MOJANG API] Failed to initialize curl.\n");
51+
return false;
52+
}
53+
54+
std::string read_buffer;
55+
bool success = false;
56+
57+
curl_easy_setopt(curl, CURLOPT_URL, url);
58+
curl_easy_setopt(curl, CURLOPT_USERAGENT, "Advancely/1.0");
59+
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, mojang_write_callback);
60+
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &read_buffer);
61+
curl_easy_setopt(curl, CURLOPT_CAINFO, CERT_BUNDLE_PATH);
62+
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
63+
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L); // 10 second timeout
64+
65+
CURLcode res = curl_easy_perform(curl);
66+
if (res == CURLE_OK) {
67+
long http_code = 0;
68+
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
69+
70+
if (http_code == 200) {
71+
cJSON *json = cJSON_Parse(read_buffer.c_str());
72+
if (json) {
73+
const cJSON *id_json = cJSON_GetObjectItem(json, "id");
74+
if (cJSON_IsString(id_json) && id_json->valuestring && strlen(id_json->valuestring) == 32) {
75+
format_uuid_with_hyphens(id_json->valuestring, out_uuid, uuid_max_len);
76+
log_message(LOG_INFO, "[MOJANG API] Fetched UUID for '%s': %s\n", username, out_uuid);
77+
success = true;
78+
} else {
79+
log_message(LOG_ERROR, "[MOJANG API] Unexpected 'id' field in response for '%s'.\n", username);
80+
}
81+
cJSON_Delete(json);
82+
} else {
83+
log_message(LOG_ERROR, "[MOJANG API] Failed to parse JSON response for '%s'.\n", username);
84+
}
85+
} else if (http_code == 404) {
86+
log_message(LOG_INFO, "[MOJANG API] Player '%s' not found (404).\n", username);
87+
} else {
88+
log_message(LOG_ERROR, "[MOJANG API] Unexpected HTTP status %ld for '%s'.\n", http_code, username);
89+
}
90+
} else {
91+
log_message(LOG_ERROR, "[MOJANG API] curl_easy_perform() failed for '%s': %s\n",
92+
username, curl_easy_strerror(res));
93+
}
94+
95+
curl_easy_cleanup(curl);
96+
return success;
97+
}

source/mojang_api.h

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright (c) 2026 LNXSeus. All Rights Reserved.
2+
//
3+
// This project is proprietary software. You are granted a license to use the software as-is.
4+
// You may not copy, distribute, modify, reverse-engineer, maintain a fork, or use this software
5+
// or its source code in any way without the express written permission of the copyright holder.
6+
//
7+
// Created by Linus on 06.04.2026.
8+
//
9+
10+
#ifndef MOJANG_API_H
11+
#define MOJANG_API_H
12+
13+
#include <cstddef> // For size_t
14+
15+
#ifdef __cplusplus
16+
extern "C" {
17+
#endif
18+
19+
/**
20+
* @brief Fetches a Minecraft player's UUID from the Mojang API.
21+
*
22+
* Sends a GET request to https://api.mojang.com/users/profiles/minecraft/<username>
23+
* and parses the UUID from the JSON response. The returned UUID is formatted with
24+
* hyphens (e.g., "069a79f4-44e9-4726-a5be-fca90e38aaf5").
25+
*
26+
* @param username The Minecraft username to look up.
27+
* @param out_uuid Buffer to store the resulting UUID string (with hyphens).
28+
* @param uuid_max_len Size of the out_uuid buffer (should be at least 48).
29+
* @return true if the UUID was successfully fetched, false on error or if the player doesn't exist.
30+
*/
31+
bool mojang_fetch_uuid(const char *username, char *out_uuid, size_t uuid_max_len);
32+
33+
#ifdef __cplusplus
34+
}
35+
#endif
36+
37+
#endif //MOJANG_API_H

source/settings.cpp

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
#include <vector>
2828

2929
#include "dialog_utils.h"
30+
#include "mojang_api.h"
3031
#include "settings_utils.h" // ImGui imported through this
3132
#include "global_event_handler.h" // For global variables
3233
#include "path_utils.h" // For path_exists()
@@ -124,10 +125,20 @@ static bool are_settings_different(const AppSettings *a, const AppSettings *b) {
124125
a->network_mode != b->network_mode ||
125126
a->coop_goal_logic != b->coop_goal_logic ||
126127
strcmp(a->host_port, b->host_port) != 0 ||
127-
strcmp(a->receiver_invite_code, b->receiver_invite_code) != 0) {
128+
strcmp(a->receiver_invite_code, b->receiver_invite_code) != 0 ||
129+
a->coop_player_count != b->coop_player_count) {
128130
return true;
129131
}
130132

133+
// Compare player roster
134+
for (int i = 0; i < a->coop_player_count; ++i) {
135+
if (strcmp(a->coop_players[i].username, b->coop_players[i].username) != 0 ||
136+
strcmp(a->coop_players[i].uuid, b->coop_players[i].uuid) != 0 ||
137+
strcmp(a->coop_players[i].display_name, b->coop_players[i].display_name) != 0) {
138+
return true;
139+
}
140+
}
141+
131142
// Compare hotkeys separately
132143
if (a->hotkey_count != b->hotkey_count) return true;
133144
for (int i = 0; i < a->hotkey_count; ++i) {
@@ -2340,6 +2351,125 @@ ImGui::SetTooltip("%s", tooltip_buffer); \
23402351
ImGui::Separator();
23412352
ImGui::Spacing();
23422353

2354+
// --- Player Roster ---
2355+
ImGui::Text("Player Roster");
2356+
ImGui::TextDisabled("Add Minecraft usernames to track. UUIDs are fetched automatically.");
2357+
ImGui::Spacing();
2358+
2359+
// Add player input
2360+
static char new_username[64] = "";
2361+
static char roster_status_msg[256] = "";
2362+
static bool roster_status_is_error = false;
2363+
2364+
ImGui::SetNextItemWidth(200.0f);
2365+
ImGui::InputText("##new_username", new_username, sizeof(new_username));
2366+
ImGui::SameLine();
2367+
bool can_add = new_username[0] != '\0' && temp_settings.coop_player_count < MAX_COOP_PLAYERS;
2368+
if (!can_add) ImGui::BeginDisabled();
2369+
if (ImGui::Button("Add Player")) {
2370+
// Check for duplicate username
2371+
bool duplicate = false;
2372+
for (int i = 0; i < temp_settings.coop_player_count; i++) {
2373+
if (strcmp(temp_settings.coop_players[i].username, new_username) == 0) {
2374+
duplicate = true;
2375+
break;
2376+
}
2377+
}
2378+
if (duplicate) {
2379+
snprintf(roster_status_msg, sizeof(roster_status_msg),
2380+
"Player '%s' is already in the roster.", new_username);
2381+
roster_status_is_error = true;
2382+
} else {
2383+
char fetched_uuid[48] = "";
2384+
bool fetched = mojang_fetch_uuid(new_username, fetched_uuid, sizeof(fetched_uuid));
2385+
if (fetched) {
2386+
CoopPlayer *p = &temp_settings.coop_players[temp_settings.coop_player_count];
2387+
memset(p, 0, sizeof(CoopPlayer));
2388+
strncpy(p->username, new_username, sizeof(p->username) - 1);
2389+
strncpy(p->uuid, fetched_uuid, sizeof(p->uuid) - 1);
2390+
p->display_name[0] = '\0';
2391+
temp_settings.coop_player_count++;
2392+
snprintf(roster_status_msg, sizeof(roster_status_msg),
2393+
"Added '%s' (UUID: %s)", new_username, fetched_uuid);
2394+
roster_status_is_error = false;
2395+
new_username[0] = '\0';
2396+
} else {
2397+
snprintf(roster_status_msg, sizeof(roster_status_msg),
2398+
"Could not find player '%s'.\n"
2399+
"Check the username.", new_username);
2400+
roster_status_is_error = true;
2401+
}
2402+
}
2403+
}
2404+
if (!can_add) ImGui::EndDisabled();
2405+
2406+
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
2407+
char tooltip_buf[256];
2408+
if (temp_settings.coop_player_count >= MAX_COOP_PLAYERS) {
2409+
snprintf(tooltip_buf, sizeof(tooltip_buf), "Roster is full (%d players max).", MAX_COOP_PLAYERS);
2410+
} else {
2411+
snprintf(tooltip_buf, sizeof(tooltip_buf),
2412+
"Type a Minecraft username and click to fetch their UUID\n"
2413+
"from the Mojang API and add them to the roster.");
2414+
}
2415+
ImGui::SetTooltip("%s", tooltip_buf);
2416+
}
2417+
2418+
// Status message
2419+
if (roster_status_msg[0] != '\0') {
2420+
if (roster_status_is_error)
2421+
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "%s", roster_status_msg);
2422+
else
2423+
ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "%s", roster_status_msg);
2424+
}
2425+
2426+
ImGui::Spacing();
2427+
2428+
// Player list
2429+
int remove_index = -1;
2430+
for (int i = 0; i < temp_settings.coop_player_count; i++) {
2431+
ImGui::PushID(i + 1000); // Offset to avoid ID conflicts with goal logic
2432+
CoopPlayer *p = &temp_settings.coop_players[i];
2433+
2434+
// Display name (editable)
2435+
ImGui::SetNextItemWidth(120.0f);
2436+
char display_label[128];
2437+
snprintf(display_label, sizeof(display_label), "##display_%d", i);
2438+
ImGui::InputTextWithHint(display_label, "Display Name", p->display_name, sizeof(p->display_name));
2439+
if (ImGui::IsItemHovered()) {
2440+
char tooltip_buf[256];
2441+
snprintf(tooltip_buf, sizeof(tooltip_buf),
2442+
"Custom display name (leave empty to use '%s').\n"
2443+
"UUID: %s",
2444+
p->username, p->uuid);
2445+
ImGui::SetTooltip("%s", tooltip_buf);
2446+
}
2447+
2448+
ImGui::SameLine();
2449+
ImGui::TextDisabled("%s", p->username);
2450+
2451+
ImGui::SameLine();
2452+
if (ImGui::SmallButton("Remove")) {
2453+
remove_index = i;
2454+
}
2455+
2456+
ImGui::PopID();
2457+
}
2458+
2459+
// Handle removal
2460+
if (remove_index >= 0) {
2461+
for (int i = remove_index; i < temp_settings.coop_player_count - 1; i++) {
2462+
temp_settings.coop_players[i] = temp_settings.coop_players[i + 1];
2463+
}
2464+
temp_settings.coop_player_count--;
2465+
memset(&temp_settings.coop_players[temp_settings.coop_player_count], 0, sizeof(CoopPlayer));
2466+
roster_status_msg[0] = '\0';
2467+
}
2468+
2469+
ImGui::Spacing();
2470+
ImGui::Separator();
2471+
ImGui::Spacing();
2472+
23432473
// Invite Code Generation (placeholder - will be functional with networking)
23442474
ImGui::Text("Invite Code");
23452475
ImGui::TextDisabled("Generate an invite code to share with receivers.");

source/settings_utils.cpp

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,8 @@ void settings_set_defaults(AppSettings *settings) {
437437
strncpy(settings->host_port, DEFAULT_HOST_PORT, sizeof(settings->host_port) - 1);
438438
settings->host_port[sizeof(settings->host_port) - 1] = '\0';
439439
settings->receiver_invite_code[0] = '\0';
440+
settings->coop_player_count = 0;
441+
memset(settings->coop_players, 0, sizeof(settings->coop_players));
440442
}
441443

442444
bool settings_load(AppSettings *settings) {
@@ -1110,6 +1112,32 @@ bool settings_load(AppSettings *settings) {
11101112
} else {
11111113
settings->receiver_invite_code[0] = '\0';
11121114
}
1115+
1116+
// Load player roster
1117+
const cJSON *roster_json = cJSON_GetObjectItem(coop_settings, "player_roster");
1118+
settings->coop_player_count = 0;
1119+
if (cJSON_IsArray(roster_json)) {
1120+
cJSON *player_item;
1121+
cJSON_ArrayForEach(player_item, roster_json) {
1122+
if (settings->coop_player_count >= MAX_COOP_PLAYERS) break;
1123+
CoopPlayer *p = &settings->coop_players[settings->coop_player_count];
1124+
memset(p, 0, sizeof(CoopPlayer));
1125+
1126+
const cJSON *uname = cJSON_GetObjectItem(player_item, "username");
1127+
if (uname && cJSON_IsString(uname)) {
1128+
strncpy(p->username, uname->valuestring, sizeof(p->username) - 1);
1129+
}
1130+
const cJSON *uid = cJSON_GetObjectItem(player_item, "uuid");
1131+
if (uid && cJSON_IsString(uid)) {
1132+
strncpy(p->uuid, uid->valuestring, sizeof(p->uuid) - 1);
1133+
}
1134+
const cJSON *dname = cJSON_GetObjectItem(player_item, "display_name");
1135+
if (dname && cJSON_IsString(dname)) {
1136+
strncpy(p->display_name, dname->valuestring, sizeof(p->display_name) - 1);
1137+
}
1138+
settings->coop_player_count++;
1139+
}
1140+
}
11131141
} else {
11141142
defaults_were_used = true;
11151143
}
@@ -1312,6 +1340,18 @@ void settings_save(const AppSettings *settings, const TemplateData *td, Settings
13121340
cJSON_AddItemToObject(coop_obj, "host_port", cJSON_CreateString(settings->host_port));
13131341
cJSON_DeleteItemFromObject(coop_obj, "receiver_invite_code");
13141342
cJSON_AddItemToObject(coop_obj, "receiver_invite_code", cJSON_CreateString(settings->receiver_invite_code));
1343+
1344+
// Save player roster
1345+
cJSON_DeleteItemFromObject(coop_obj, "player_roster");
1346+
cJSON *roster_arr = cJSON_CreateArray();
1347+
for (int i = 0; i < settings->coop_player_count && i < MAX_COOP_PLAYERS; i++) {
1348+
cJSON *player_obj = cJSON_CreateObject();
1349+
cJSON_AddItemToObject(player_obj, "username", cJSON_CreateString(settings->coop_players[i].username));
1350+
cJSON_AddItemToObject(player_obj, "uuid", cJSON_CreateString(settings->coop_players[i].uuid));
1351+
cJSON_AddItemToObject(player_obj, "display_name", cJSON_CreateString(settings->coop_players[i].display_name));
1352+
cJSON_AddItemToArray(roster_arr, player_obj);
1353+
}
1354+
cJSON_AddItemToObject(coop_obj, "player_roster", roster_arr);
13151355
}
13161356

13171357

source/settings_utils.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ extern const char *TRACKER_SECTION_NAMES[SECTION_COUNT];
6464

6565
#define MAX_WORLD_NOTES 32 // Limit for amount of per-world notes until they delete itself
6666
#define MAX_HOTKEYS 32 // Limit for amount of hotkeys
67+
#define MAX_COOP_PLAYERS 32 // Maximum number of players in a co-op session
6768

6869
// DEFAULT values
6970
#define DEFAULT_ENABLE_OVERLAY false // Stream overlay will be off by default
@@ -147,6 +148,13 @@ extern const char *TRACKER_SECTION_NAMES[SECTION_COUNT];
147148

148149
struct TemplateData;
149150

151+
// A player in the co-op roster (Host tracks these)
152+
typedef struct {
153+
char username[64]; // Minecraft username e.g., Notch
154+
char uuid[48]; // UUID from Mojang API (with hyphens, e.g., "069a79f4-44e9-4726-a5be-fca90e38aaf5")
155+
char display_name[64]; // Optional custom display name (empty = use username)
156+
} CoopPlayer;
157+
150158
typedef struct {
151159
char target_goal[192];
152160
char increment_key[32];
@@ -319,6 +327,10 @@ struct AppSettings {
319327
CoopGoalLogic coop_goal_logic; // How to merge progress from multiple players
320328
char host_port[16]; // The port the host listens on (default "25565" - Force Port Mod)
321329
char receiver_invite_code[512]; // Base64-encoded connection string pasted by receivers
330+
331+
// --- Player Roster (Host only) ---
332+
int coop_player_count; // Number of players in the roster
333+
CoopPlayer coop_players[MAX_COOP_PLAYERS]; // The player roster
322334
};
323335

324336

0 commit comments

Comments
 (0)