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