yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
agent_collaboration_coordinator.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cctype>
5#include <cstdlib>
6#include <cstring>
7#include <filesystem>
8#include <fstream>
9#include <random>
10#include <set>
11#include <string>
12#include <utility>
13
14#include "absl/status/status.h"
15#include "absl/status/statusor.h"
16#include "absl/strings/ascii.h"
17#include "absl/strings/str_cat.h"
18#include "absl/strings/str_format.h"
19#include "absl/strings/strip.h"
20#include "util/file_util.h"
21#include "util/platform_paths.h"
22#include "util/macro.h"
23
24#include <filesystem>
25namespace fs = std::filesystem;
26
27namespace yaze {
28namespace editor {
29
30namespace {
31
32std::filesystem::path ExpandUserPath(std::string path) {
33 if (!path.empty() && path.front() == '~') {
34 const char* home = nullptr;
35#ifdef _WIN32
36 home = std::getenv("USERPROFILE");
37#else
38 home = std::getenv("HOME");
39#endif
40 if (home != nullptr) {
41 path.replace(0, 1, home);
42 }
43 }
44 return std::filesystem::path(path);
45}
46
47std::string Trimmed(const std::string& value) {
48 return std::string(absl::StripAsciiWhitespace(value));
49}
50
51} // namespace
52
55
56absl::StatusOr<AgentCollaborationCoordinator::SessionInfo>
57AgentCollaborationCoordinator::HostSession(const std::string& session_name) {
58 const std::string trimmed = Trimmed(session_name);
59 if (trimmed.empty()) {
60 return absl::InvalidArgumentError("Session name cannot be empty");
61 }
62
64
65 SessionFileData data;
66 data.session_name = trimmed;
68 data.host = local_user_;
69 data.participants.push_back(local_user_);
70
71 std::filesystem::path path = SessionFilePath(data.session_code);
72
73 // Collision avoidance (extremely unlikely but cheap to guard against).
74 int attempts = 0;
75 while (std::filesystem::exists(path) && attempts++ < 5) {
77 path = SessionFilePath(data.session_code);
78 }
79 if (std::filesystem::exists(path)) {
80 return absl::InternalError(
81 "Unable to allocate a new collaboration session code");
82 }
83
85
86 active_ = true;
87 hosting_ = true;
90
91 SessionInfo info;
92 info.session_id = data.session_code;
93 info.session_name = data.session_name;
94 info.participants = data.participants;
95 return info;
96}
97
98absl::StatusOr<AgentCollaborationCoordinator::SessionInfo>
99AgentCollaborationCoordinator::JoinSession(const std::string& session_code) {
100 const std::string normalized = NormalizeSessionCode(session_code);
101 if (normalized.empty()) {
102 return absl::InvalidArgumentError("Session code cannot be empty");
103 }
104
106
107 std::filesystem::path path = SessionFilePath(normalized);
109
110 const auto already_joined = std::find(data.participants.begin(),
111 data.participants.end(), local_user_);
112 if (already_joined == data.participants.end()) {
113 data.participants.push_back(local_user_);
115 }
116
117 active_ = true;
118 hosting_ = false;
119 session_id_ = data.session_code.empty() ? normalized : data.session_code;
120 session_name_ = data.session_name.empty() ? session_id_ : data.session_name;
121
122 SessionInfo info;
123 info.session_id = session_id_;
125 info.participants = data.participants;
126 return info;
127}
128
130 if (!active_) {
131 return absl::FailedPreconditionError("No collaborative session active");
132 }
133
134 const std::filesystem::path path = SessionFilePath(session_id_);
135 absl::Status status = absl::OkStatus();
136
137 if (hosting_) {
138 std::error_code ec;
139 std::filesystem::remove(path, ec);
140 if (ec) {
141 status = absl::InternalError(
142 absl::StrFormat("Failed to clean up session file: %s", ec.message()));
143 }
144 } else {
145 auto data_or = LoadSessionFile(path);
146 if (data_or.ok()) {
147 SessionFileData data = std::move(data_or.value());
148 auto end = std::remove(data.participants.begin(), data.participants.end(),
150 data.participants.erase(end, data.participants.end());
151
152 if (data.participants.empty()) {
153 std::error_code ec;
154 std::filesystem::remove(path, ec);
155 if (ec) {
156 status = absl::InternalError(absl::StrFormat(
157 "Failed to remove empty session file: %s", ec.message()));
158 }
159 } else {
160 status = WriteSessionFile(path, data);
161 }
162 } else {
163 // If the session file has already disappeared, treat it as success.
164 status = absl::OkStatus();
165 }
166 }
167
168 active_ = false;
169 hosting_ = false;
170 session_id_.clear();
171 session_name_.clear();
172
173 return status;
174}
175
176absl::StatusOr<AgentCollaborationCoordinator::SessionInfo>
178 if (!active_) {
179 return absl::FailedPreconditionError("No collaborative session active");
180 }
181
182 const std::filesystem::path path = SessionFilePath(session_id_);
183 auto data_or = LoadSessionFile(path);
184 if (!data_or.ok()) {
185 absl::Status status = data_or.status();
186 if (absl::IsNotFound(status)) {
187 active_ = false;
188 hosting_ = false;
189 session_id_.clear();
190 session_name_.clear();
191 }
192 return status;
193 }
194
195 SessionFileData data = std::move(data_or.value());
196 session_name_ = data.session_name.empty() ? session_id_ : data.session_name;
197 SessionInfo info;
198 info.session_id = session_id_;
200 info.participants = data.participants;
201 return info;
202}
203
205 std::error_code ec;
206 std::filesystem::create_directories(SessionsDirectory(), ec);
207 if (ec) {
208 return absl::InternalError(absl::StrFormat(
209 "Failed to create collaboration directory: %s", ec.message()));
210 }
211 return absl::OkStatus();
212}
213
215 const char* override_name = std::getenv("YAZE_USER_NAME");
216 const char* user = override_name != nullptr ? override_name : std::getenv("USER");
217 if (user == nullptr) {
218 user = std::getenv("USERNAME");
219 }
220 std::string base = (user != nullptr && std::strlen(user) > 0)
221 ? std::string(user)
222 : std::string("Player");
223
224 const char* host = std::getenv("HOSTNAME");
225#if defined(_WIN32)
226 if (host == nullptr) {
227 host = std::getenv("COMPUTERNAME");
228 }
229#endif
230 if (host != nullptr && std::strlen(host) > 0) {
231 return absl::StrCat(base, "@", host);
232 }
233 return base;
234}
235
237 const std::string& input) const {
238 std::string normalized = Trimmed(input);
239 normalized.erase(std::remove_if(normalized.begin(), normalized.end(),
240 [](unsigned char c) {
241 return !std::isalnum(
242 static_cast<unsigned char>(c));
243 }),
244 normalized.end());
245 std::transform(normalized.begin(), normalized.end(), normalized.begin(),
246 [](unsigned char c) {
247 return static_cast<char>(
248 std::toupper(static_cast<unsigned char>(c)));
249 });
250 return normalized;
251}
252
254 static constexpr char kAlphabet[] = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
255 thread_local std::mt19937 rng{std::random_device{}()};
256 std::uniform_int_distribution<size_t> dist(0, sizeof(kAlphabet) - 2);
257
258 std::string code(6, '0');
259 for (char& ch : code) {
260 ch = kAlphabet[dist(rng)];
261 }
262 return code;
263}
264
266 auto config_dir = util::PlatformPaths::GetConfigDirectory();
267 if (!config_dir.ok()) {
268 // Fallback to a local directory if config can't be determined.
269 return fs::current_path() / ".yaze" / "agent" / "sessions";
270 }
271 return *config_dir / "agent" / "sessions";
272}
273
275 const std::string& code) const {
276 return SessionsDirectory() / (code + ".session");
277}
278
279absl::StatusOr<AgentCollaborationCoordinator::SessionFileData>
281 const std::filesystem::path& path) const {
282 std::ifstream file(path);
283 if (!file.is_open()) {
284 return absl::NotFoundError(
285 absl::StrFormat("Session %s does not exist", path.string()));
286 }
287
288 SessionFileData data;
289 data.session_code = path.stem().string();
290
291 std::string line;
292 while (std::getline(file, line)) {
293 auto pos = line.find(':');
294 if (pos == std::string::npos) {
295 continue;
296 }
297 std::string key = line.substr(0, pos);
298 std::string value = Trimmed(line.substr(pos + 1));
299 if (key == "name") {
300 data.session_name = value;
301 } else if (key == "code") {
303 } else if (key == "host") {
304 data.host = value;
305 data.participants.push_back(value);
306 } else if (key == "participant") {
307 if (std::find(data.participants.begin(), data.participants.end(), value) ==
308 data.participants.end()) {
309 data.participants.push_back(value);
310 }
311 }
312 }
313
314 if (data.session_name.empty()) {
315 data.session_name = data.session_code;
316 }
317 if (!data.host.empty()) {
318 auto host_it = std::find(data.participants.begin(), data.participants.end(),
319 data.host);
320 if (host_it == data.participants.end()) {
321 data.participants.insert(data.participants.begin(), data.host);
322 } else if (host_it != data.participants.begin()) {
323 std::rotate(data.participants.begin(), host_it,
324 std::next(host_it));
325 }
326 }
327
328 return data;
329}
330
332 const std::filesystem::path& path, const SessionFileData& data) const {
333 std::ofstream file(path, std::ios::trunc);
334 if (!file.is_open()) {
335 return absl::InternalError(
336 absl::StrFormat("Failed to write session file: %s", path.string()));
337 }
338
339 file << "name:" << data.session_name << "\n";
340 file << "code:" << data.session_code << "\n";
341 file << "host:" << data.host << "\n";
342
343 std::set<std::string> seen;
344 seen.insert(data.host);
345 for (const auto& participant : data.participants) {
346 if (seen.insert(participant).second) {
347 file << "participant:" << participant << "\n";
348 }
349 }
350
351 file.flush();
352 if (!file.good()) {
353 return absl::InternalError(
354 absl::StrFormat("Failed to flush session file: %s", path.string()));
355 }
356 return absl::OkStatus();
357}
358
359} // namespace editor
360} // namespace yaze
absl::StatusOr< SessionInfo > JoinSession(const std::string &session_code)
absl::StatusOr< SessionFileData > LoadSessionFile(const std::filesystem::path &path) const
std::string NormalizeSessionCode(const std::string &input) const
absl::Status WriteSessionFile(const std::filesystem::path &path, const SessionFileData &data) const
absl::StatusOr< SessionInfo > HostSession(const std::string &session_name)
std::filesystem::path SessionFilePath(const std::string &code) const
static absl::StatusOr< std::filesystem::path > GetConfigDirectory()
Get the user-specific configuration directory for YAZE.
#define RETURN_IF_ERROR(expression)
Definition macro.h:53
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:61
Main namespace for the application.