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 config_dir = util::PlatformPaths::GetConfigDirectory();
266 if (!config_dir.ok()) {
267 // Fallback to a local directory if config can't be determined.
268 return fs::current_path() / ".yaze" / "agent" / "sessions";
269 }
270 return *config_dir / "agent" / "sessions";
271}
272
274 const std::string& code) const {
275 return SessionsDirectory() / (code + ".session");
276}
277
278absl::StatusOr<AgentCollaborationCoordinator::SessionFileData>
280 const std::filesystem::path& path) const {
281 std::ifstream file(path);
282 if (!file.is_open()) {
283 return absl::NotFoundError(
284 absl::StrFormat("Session %s does not exist", path.string()));
285 }
286
287 SessionFileData data;
288 data.session_code = path.stem().string();
289
290 std::string line;
291 while (std::getline(file, line)) {
292 auto pos = line.find(':');
293 if (pos == std::string::npos) {
294 continue;
295 }
296 std::string key = line.substr(0, pos);
297 std::string value = Trimmed(line.substr(pos + 1));
298 if (key == "name") {
299 data.session_name = value;
300 } else if (key == "code") {
302 } else if (key == "host") {
303 data.host = value;
304 data.participants.push_back(value);
305 } else if (key == "participant") {
306 if (std::find(data.participants.begin(), data.participants.end(),
307 value) == data.participants.end()) {
308 data.participants.push_back(value);
309 }
310 }
311 }
312
313 if (data.session_name.empty()) {
314 data.session_name = data.session_code;
315 }
316 if (!data.host.empty()) {
317 auto host_it = std::find(data.participants.begin(), data.participants.end(),
318 data.host);
319 if (host_it == data.participants.end()) {
320 data.participants.insert(data.participants.begin(), data.host);
321 } else if (host_it != data.participants.begin()) {
322 std::rotate(data.participants.begin(), host_it, std::next(host_it));
323 }
324 }
325
326 return data;
327}
328
330 const std::filesystem::path& path, const SessionFileData& data) const {
331 std::ofstream file(path, std::ios::trunc);
332 if (!file.is_open()) {
333 return absl::InternalError(
334 absl::StrFormat("Failed to write session file: %s", path.string()));
335 }
336
337 file << "name:" << data.session_name << "\n";
338 file << "code:" << data.session_code << "\n";
339 file << "host:" << data.host << "\n";
340
341 std::set<std::string> seen;
342 seen.insert(data.host);
343 for (const auto& participant : data.participants) {
344 if (seen.insert(participant).second) {
345 file << "participant:" << participant << "\n";
346 }
347 }
348
349 file.flush();
350 if (!file.good()) {
351 return absl::InternalError(
352 absl::StrFormat("Failed to flush session file: %s", path.string()));
353 }
354 return absl::OkStatus();
355}
356
357} // namespace editor
358} // 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 ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22