yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
api_handlers.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cctype>
5#include <filesystem>
6#include <fstream>
7#include <sstream>
8#include <system_error>
9
10#include "absl/status/status.h"
16#include "httplib.h"
17#include "nlohmann/json.hpp"
18#include "rom/rom.h"
19#include "util/log.h"
20
21namespace yaze {
22namespace cli {
23namespace api {
24
25using json = nlohmann::json;
26
27namespace {
28
29constexpr const char* kCorsAllowOrigin = "*";
30constexpr const char* kCorsAllowHeaders = "Content-Type, Authorization, Accept";
31constexpr const char* kCorsAllowMethods = "GET, POST, OPTIONS";
32constexpr const char* kCorsMaxAge = "86400";
33
34bool IsTruthyParam(const std::string& value, bool param_present) {
35 if (value.empty()) {
36 return param_present;
37 }
38 std::string normalized = value;
39 std::transform(
40 normalized.begin(), normalized.end(), normalized.begin(),
41 [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
42 return normalized == "1" || normalized == "true" || normalized == "yes" ||
43 normalized == "on";
44}
45
46} // namespace
47
48void ApplyCorsHeaders(httplib::Response& res) {
49 res.set_header("Access-Control-Allow-Origin", kCorsAllowOrigin);
50 res.set_header("Access-Control-Allow-Headers", kCorsAllowHeaders);
51 res.set_header("Access-Control-Allow-Methods", kCorsAllowMethods);
52 res.set_header("Access-Control-Max-Age", kCorsMaxAge);
53}
54
55namespace {
56
57void HandleWindowAction(const std::function<bool()>& action,
58 const char* window_state, httplib::Response& res) {
60
61 if (action) {
62 if (action()) {
63 json response;
64 response["status"] = "ok";
65 response["window"] = window_state;
66 res.status = 200;
67 res.set_content(response.dump(), "application/json");
68 return;
69 }
70 res.status = 500;
71 json response;
72 response["status"] = "error";
73 response["message"] = "window action failed";
74 res.set_content(response.dump(), "application/json");
75 return;
76 }
77
78 res.status = 501;
79 json response;
80 response["status"] = "error";
81 response["message"] = "window control unavailable";
82 res.set_content(response.dump(), "application/json");
83}
84
85} // namespace
86
87void HandleHealth(const httplib::Request& req, httplib::Response& res,
88 const BonjourPublisher* bonjour) {
89 (void)req;
90 json j;
91 j["status"] = "ok";
92 j["version"] = "1.0";
93 j["service"] = "yaze-agent-api";
94
95 // Report LAN discovery mechanism availability.
96 if (bonjour && bonjour->IsAvailable()) {
97 j["discovery"] = "bonjour";
98 } else {
99 j["discovery"] = "none";
100 }
101
102 res.status = 200;
103 res.set_content(j.dump(), "application/json");
104 ApplyCorsHeaders(res);
105}
106
107void HandleListModels(const httplib::Request& req, httplib::Response& res) {
108 auto& registry = ModelRegistry::GetInstance();
109 const bool force_refresh =
110 IsTruthyParam(req.get_param_value("refresh"), req.has_param("refresh"));
111 auto models_or = registry.ListAllModels(force_refresh);
112
113 ApplyCorsHeaders(res);
114
115 if (!models_or.ok()) {
116 json j;
117 j["error"] = models_or.status().message();
118 res.status = 500;
119 res.set_content(j.dump(), "application/json");
120 return;
121 }
122
123 json j_models = json::array();
124 for (const auto& info : *models_or) {
125 json j_model;
126 j_model["name"] = info.name;
127 j_model["display_name"] = info.display_name;
128 j_model["provider"] = info.provider;
129 j_model["description"] = info.description;
130 j_model["family"] = info.family;
131 j_model["parameter_size"] = info.parameter_size;
132 j_model["quantization"] = info.quantization;
133 j_model["size_bytes"] = info.size_bytes;
134 j_model["is_local"] = info.is_local;
135 j_models.push_back(j_model);
136 }
137
138 json response;
139 response["models"] = j_models;
140 response["count"] = j_models.size();
141
142 res.status = 200;
143 res.set_content(response.dump(), "application/json");
144}
145
146void HandleGetSymbols(const httplib::Request& req, httplib::Response& res,
148 ApplyCorsHeaders(res);
149
150 std::string format_str = req.get_param_value("format");
151 if (format_str.empty())
152 format_str = "mesen";
153
156 if (format_str == "mesen") {
158 } else if (format_str == "asar") {
160 } else if (format_str == "wla") {
162 } else if (format_str == "bsnes") {
164 } else {
165 json j;
166 j["error"] = "Unsupported symbol format";
167 j["format"] = format_str;
168 j["supported"] = {"mesen", "asar", "wla", "bsnes"};
169 res.status = 400;
170 res.set_content(j.dump(), "application/json");
171 return;
172 }
173
174 if (!symbols) {
175 json j;
176 j["error"] = "Symbol provider not available";
177 res.status = 503;
178 res.set_content(j.dump(), "application/json");
179 return;
180 }
181
182 auto export_or = symbols->ExportSymbols(format);
183 if (export_or.ok()) {
184 const std::string accept = req.has_header("Accept")
185 ? req.get_header_value("Accept")
186 : std::string();
187 const bool wants_json =
188 accept.find("application/json") != std::string::npos;
189 if (wants_json) {
190 json j;
191 j["symbols"] = *export_or;
192 j["format"] = format_str;
193 res.status = 200;
194 res.set_content(j.dump(), "application/json");
195 } else {
196 res.status = 200;
197 res.set_content(*export_or, "text/plain");
198 }
199 } else {
200 json j;
201 j["error"] = export_or.status().message();
202 res.status = 500;
203 res.set_content(j.dump(), "application/json");
204 }
205}
206
207void HandleNavigate(const httplib::Request& req, httplib::Response& res) {
208 ApplyCorsHeaders(res);
209
210 try {
211 json body = json::parse(req.body);
212 uint32_t address = body.value("address", 0);
213 std::string source = body.value("source", "unknown");
214
215 (void)source; // Used in response
216 // Navigate request logged via JSON response
217
218 // TODO: Integrate with yaze's disassembly viewer to jump to address
219 // For now, just acknowledge the request
220 json response;
221 response["status"] = "ok";
222 response["address"] = address;
223 response["message"] = "Navigation request received";
224 res.status = 200;
225 res.set_content(response.dump(), "application/json");
226 } catch (const std::exception& e) {
227 json j;
228 j["error"] = e.what();
229 res.status = 400;
230 res.set_content(j.dump(), "application/json");
231 }
232}
233
234void HandleBreakpointHit(const httplib::Request& req, httplib::Response& res) {
235 ApplyCorsHeaders(res);
236
237 try {
238 json body = json::parse(req.body);
239 uint32_t address = body.value("address", 0);
240 std::string source = body.value("source", "unknown");
241
242 (void)source; // Used in response
243
244 // Log CPU state if provided (for debugging, stored in response)
245 json cpu_info;
246 if (body.contains("cpu_state")) {
247 cpu_info = body["cpu_state"];
248 }
249
250 // TODO: Integrate with RomDebugAgent for analysis
251 json response;
252 response["status"] = "ok";
253 response["address"] = address;
254 res.status = 200;
255 res.set_content(response.dump(), "application/json");
256 } catch (const std::exception& e) {
257 json j;
258 j["error"] = e.what();
259 res.status = 400;
260 res.set_content(j.dump(), "application/json");
261 }
262}
263
264void HandleStateUpdate(const httplib::Request& req, httplib::Response& res) {
265 ApplyCorsHeaders(res);
266
267 try {
268 json body = json::parse(req.body);
269
270 // State update received; store for panel use
271 (void)body; // Will be used when state storage is implemented
272
273 // TODO: Store state for use by MesenDebugPanel and RomDebugAgent
274 json response;
275 response["status"] = "ok";
276 res.status = 200;
277 res.set_content(response.dump(), "application/json");
278 } catch (const std::exception& e) {
279 json j;
280 j["error"] = e.what();
281 res.status = 400;
282 res.set_content(j.dump(), "application/json");
283 }
284}
285
286void HandleWindowShow(const httplib::Request& req, httplib::Response& res,
287 const std::function<bool()>& action) {
288 (void)req;
289 HandleWindowAction(action, "shown", res);
290}
291
292void HandleWindowHide(const httplib::Request& req, httplib::Response& res,
293 const std::function<bool()>& action) {
294 (void)req;
295 HandleWindowAction(action, "hidden", res);
296}
297
298void HandleCorsPreflight(const httplib::Request& req, httplib::Response& res) {
299 (void)req;
300 ApplyCorsHeaders(res);
301 res.status = 204;
302}
303
304// ---------------------------------------------------------------------------
305// Render endpoints
306// ---------------------------------------------------------------------------
307
308namespace {
309
310// Parse "room" query param — accepts decimal or 0x-prefixed hex.
311// Returns false and sets a 400 error response on failure.
312bool ParseRoomParam(const httplib::Request& req, httplib::Response& res,
313 int& room_id) {
314 if (!req.has_param("room")) {
315 json j;
316 j["error"] = "Missing required parameter: room";
317 res.status = 400;
318 res.set_content(j.dump(), "application/json");
319 return false;
320 }
321 const std::string s = req.get_param_value("room");
322 try {
323 room_id = std::stoi(s, nullptr, 0);
324 } catch (...) {
325 json j;
326 j["error"] = "Invalid room parameter";
327 j["value"] = s;
328 res.status = 400;
329 res.set_content(j.dump(), "application/json");
330 return false;
331 }
332 return true;
333}
334
335} // namespace
336
337void HandleRenderDungeon(const httplib::Request& req, httplib::Response& res,
338 yaze::app::service::RenderService* render_service) {
339 ApplyCorsHeaders(res);
340
341 if (!render_service) {
342 json j;
343 j["error"] = "Render service not available";
344 res.status = 503;
345 res.set_content(j.dump(), "application/json");
346 return;
347 }
348
349 int room_id = 0;
350 if (!ParseRoomParam(req, res, room_id))
351 return;
352
353 // Parse overlays — comma-separated: collision, sprites, objects, track,
354 // camera, grid, tiletype, all.
355 uint32_t overlay_flags = yaze::app::service::RenderOverlay::kNone;
356 if (req.has_param("overlays")) {
357 std::istringstream ss(req.get_param_value("overlays"));
358 std::string tok;
359 while (std::getline(ss, tok, ',')) {
360 tok.erase(0, tok.find_first_not_of(" \t"));
361 if (!tok.empty())
362 tok.erase(tok.find_last_not_of(" \t") + 1);
363 if (tok == "collision")
365 else if (tok == "sprites")
367 else if (tok == "objects")
369 else if (tok == "track")
371 else if (tok == "camera")
373 else if (tok == "grid")
375 else if (tok == "tiletype")
377 else if (tok == "all")
379 }
380 }
381
382 // Parse scale strictly; malformed requests must not silently render at 1x.
383 float scale = 1.0f;
384 if (req.has_param("scale")) {
385 const auto scale_or =
386 yaze::app::service::ParseRenderScale(req.get_param_value("scale"));
387 if (!scale_or.ok()) {
388 json j;
389 j["error"] = std::string(scale_or.status().message());
390 res.status = 400;
391 res.set_content(j.dump(), "application/json");
392 return;
393 }
394 scale = *scale_or;
395 }
396
398 render_req.room_id = room_id;
399 render_req.overlay_flags = overlay_flags;
400 render_req.scale = scale;
401
402 auto result_or = render_service->RenderDungeonRoom(render_req);
403 if (!result_or.ok()) {
404 const auto& s = result_or.status();
405 json j;
406 j["error"] = std::string(s.message());
407 if (s.code() == absl::StatusCode::kInvalidArgument)
408 res.status = 400;
409 else if (s.code() == absl::StatusCode::kFailedPrecondition)
410 res.status = 503;
411 else
412 res.status = 500;
413 res.set_content(j.dump(), "application/json");
414 return;
415 }
416
417 const auto& result = *result_or;
418 res.status = 200;
419 res.set_content(reinterpret_cast<const char*>(result.png_data.data()),
420 result.png_data.size(), "image/png");
421 res.set_header("X-Room-Id", std::to_string(room_id));
422 res.set_header("X-Room-Width", std::to_string(result.width));
423 res.set_header("X-Room-Height", std::to_string(result.height));
424}
425
427 const httplib::Request& req, httplib::Response& res,
428 yaze::app::service::RenderService* render_service) {
429 ApplyCorsHeaders(res);
430
431 if (!render_service) {
432 json j;
433 j["error"] = "Render service not available";
434 res.status = 503;
435 res.set_content(j.dump(), "application/json");
436 return;
437 }
438
439 int room_id = 0;
440 if (!ParseRoomParam(req, res, room_id))
441 return;
442
443 auto meta_or = render_service->GetDungeonRoomMetadata(room_id);
444 if (!meta_or.ok()) {
445 const auto& s = meta_or.status();
446 json j;
447 j["error"] = std::string(s.message());
448 res.status = (s.code() == absl::StatusCode::kInvalidArgument) ? 400 : 500;
449 res.set_content(j.dump(), "application/json");
450 return;
451 }
452
453 const auto& meta = *meta_or;
454 json j;
455 j["room_id"] = meta.room_id;
456 j["blockset"] = meta.blockset;
457 j["spriteset"] = meta.spriteset;
458 j["palette"] = meta.palette;
459 j["layout_id"] = meta.layout_id;
460 j["effect"] = meta.effect;
461 j["collision"] = meta.collision;
462 j["tag1"] = meta.tag1;
463 j["tag2"] = meta.tag2;
464 j["message_id"] = meta.message_id;
465 j["has_custom_collision"] = meta.has_custom_collision;
466 j["object_count"] = meta.object_count;
467 j["sprite_count"] = meta.sprite_count;
468 res.status = 200;
469 res.set_content(j.dump(), "application/json");
470}
471
472// ---------------------------------------------------------------------------
473// Command execution endpoints (Phase 3)
474// ---------------------------------------------------------------------------
475
477 const httplib::Request& req, httplib::Response& res,
478 const std::function<absl::StatusOr<std::string>(const std::string&)>&
479 action) {
480 // Local processes opt in explicitly. Reject browser origins even if another
481 // API route grants CORS; a web page must never switch a user's project.
482 if ((req.remote_addr != "127.0.0.1" && req.remote_addr != "::1") ||
483 req.has_header("Origin") ||
484 req.get_header_value("X-Yaze-Control") != "1") {
485 res.status = 403;
486 res.set_content(
487 R"({"protocol":"yaze-editor-control/v1","status":"error","error":"Local editor client required"})",
488 "application/json");
489 return;
490 }
491 if (!action || req.body.size() > 16384) {
492 res.status = action ? 413 : 503;
493 res.set_content(
494 R"({"protocol":"yaze-editor-control/v1","status":"error","error":"Editor control unavailable or request too large"})",
495 "application/json");
496 return;
497 }
498 auto result = action(req.body);
499 if (!result.ok()) {
500 res.status = absl::IsDeadlineExceeded(result.status()) ? 504 : 503;
501 res.set_content(json({{"protocol", "yaze-editor-control/v1"},
502 {"status", "error"},
503 {"error", std::string(result.status().message())}})
504 .dump(),
505 "application/json");
506 return;
507 }
508 const auto response = json::parse(*result, nullptr, false);
509 res.status = response.is_object() ? response.value("http_status", 200) : 500;
510 res.set_content(*result, "application/json");
511}
512
513void HandleCommandExecute(const httplib::Request& req, httplib::Response& res,
514 yaze::Rom* rom) {
515 ApplyCorsHeaders(res);
516
517 try {
518 json body = json::parse(req.body);
519 std::string command_name = body.value("command", "");
520 if (command_name.empty()) {
521 json j;
522 j["error"] = "Missing 'command' field";
523 res.status = 400;
524 res.set_content(j.dump(), "application/json");
525 return;
526 }
527
528 std::vector<std::string> args;
529 if (body.contains("args") && body["args"].is_array()) {
530 for (const auto& arg : body["args"]) {
531 args.push_back(arg.get<std::string>());
532 }
533 }
534
535 auto& registry = CommandRegistry::Instance();
536 if (!registry.HasCommand(command_name)) {
537 json j;
538 j["error"] = "Unknown command: " + command_name;
539 res.status = 404;
540 res.set_content(j.dump(), "application/json");
541 return;
542 }
543
544 std::string captured_output;
545 auto status = registry.Execute(command_name, args, rom, &captured_output);
546
547 json response;
548 if (status.ok()) {
549 response["status"] = "ok";
550 response["command"] = command_name;
551 // Try to parse output as JSON, fall back to string
552 try {
553 response["result"] = json::parse(captured_output);
554 } catch (...) {
555 response["result"] = captured_output;
556 }
557 res.status = 200;
558 } else {
559 response["status"] = "error";
560 response["command"] = command_name;
561 response["error"] = std::string(status.message());
562 if (!captured_output.empty()) {
563 response["output"] = captured_output;
564 }
565 res.status = 500;
566 }
567 res.set_content(response.dump(), "application/json");
568 } catch (const std::exception& e) {
569 json j;
570 j["error"] = e.what();
571 res.status = 400;
572 res.set_content(j.dump(), "application/json");
573 }
574}
575
576void HandleCommandExecuteDisabled(const httplib::Request& req,
577 httplib::Response& res) {
578 (void)req;
579 ApplyCorsHeaders(res);
580 json j;
581 j["status"] = "error";
582 j["error"] = "Command execution is disabled on this server";
583 res.status = 403;
584 res.set_content(j.dump(), "application/json");
585}
586
587void HandleCommandList(const httplib::Request& req, httplib::Response& res) {
588 (void)req;
589 ApplyCorsHeaders(res);
590
591 auto& registry = CommandRegistry::Instance();
592 json j_commands = json::array();
593
594 for (const auto& category : registry.GetCategories()) {
595 for (const auto& cmd_name : registry.GetCommandsInCategory(category)) {
596 const auto* meta = registry.GetMetadata(cmd_name);
597 if (!meta)
598 continue;
599
600 json j_cmd;
601 j_cmd["name"] = meta->name;
602 j_cmd["category"] = meta->category;
603 j_cmd["description"] = meta->description;
604 j_cmd["usage"] = meta->usage;
605 j_cmd["requires_rom"] = meta->requires_rom;
606 j_commands.push_back(j_cmd);
607 }
608 }
609
610 json response;
611 response["commands"] = j_commands;
612 response["count"] = j_commands.size();
613 res.status = 200;
614 res.set_content(response.dump(), "application/json");
615}
616
617// ---------------------------------------------------------------------------
618// Annotation CRUD endpoints (Phase 4)
619// ---------------------------------------------------------------------------
620
621namespace {
622
623std::string ResolveAnnotationsPath(const std::string& project_path) {
624 if (project_path.empty())
625 return "";
626 // Match the iOS AnnotationStore resolution logic
627 return project_path + "/Docs/Dev/Planning/annotations.json";
628}
629
630json LoadAnnotationsFile(const std::string& path) {
631 if (path.empty())
632 return json::object();
633 std::ifstream file(path);
634 if (!file.is_open())
635 return json::object();
636 try {
637 return json::parse(file);
638 } catch (...) {
639 return json::object();
640 }
641}
642
643bool SaveAnnotationsFile(const std::string& path, const json& data) {
644 if (path.empty())
645 return false;
646
647 const std::filesystem::path parent =
648 std::filesystem::path(path).parent_path();
649 if (!parent.empty()) {
650 std::error_code error;
651 std::filesystem::create_directories(parent, error);
652 if (error) {
653 return false;
654 }
655 }
656
657 std::ofstream file(path);
658 if (!file.is_open())
659 return false;
660 file << data.dump(2);
661 return file.good();
662}
663
664} // namespace
665
666void HandleAnnotationList(const httplib::Request& req, httplib::Response& res,
667 const std::string& project_path) {
668 ApplyCorsHeaders(res);
669
670 std::string path = ResolveAnnotationsPath(project_path);
671 if (path.empty()) {
672 json j;
673 j["error"] = "No project path configured";
674 res.status = 503;
675 res.set_content(j.dump(), "application/json");
676 return;
677 }
678
679 json file_data = LoadAnnotationsFile(path);
680 json annotations = file_data.value("annotations", json::array());
681
682 // Optional room filter
683 if (req.has_param("room")) {
684 int room_id = 0;
685 try {
686 room_id = std::stoi(req.get_param_value("room"), nullptr, 0);
687 } catch (...) {
688 json j;
689 j["error"] = "Invalid room parameter";
690 res.status = 400;
691 res.set_content(j.dump(), "application/json");
692 return;
693 }
694
695 json filtered = json::array();
696 for (const auto& ann : annotations) {
697 if (ann.value("room_id", -1) == room_id) {
698 filtered.push_back(ann);
699 }
700 }
701 annotations = filtered;
702 }
703
704 json response;
705 response["annotations"] = annotations;
706 res.status = 200;
707 res.set_content(response.dump(), "application/json");
708}
709
710void HandleAnnotationCreate(const httplib::Request& req, httplib::Response& res,
711 const std::string& project_path) {
712 ApplyCorsHeaders(res);
713
714 std::string path = ResolveAnnotationsPath(project_path);
715 if (path.empty()) {
716 json j;
717 j["error"] = "No project path configured";
718 res.status = 503;
719 res.set_content(j.dump(), "application/json");
720 return;
721 }
722
723 try {
724 json new_annotation = json::parse(req.body);
725 json file_data = LoadAnnotationsFile(path);
726
727 if (!file_data.contains("annotations")) {
728 file_data["annotations"] = json::array();
729 }
730 file_data["annotations"].push_back(new_annotation);
731
732 if (SaveAnnotationsFile(path, file_data)) {
733 json response;
734 response["status"] = "ok";
735 response["id"] = new_annotation.value("id", "");
736 res.status = 201;
737 res.set_content(response.dump(), "application/json");
738 } else {
739 json j;
740 j["error"] = "Failed to write annotations file";
741 res.status = 500;
742 res.set_content(j.dump(), "application/json");
743 }
744 } catch (const std::exception& e) {
745 json j;
746 j["error"] = e.what();
747 res.status = 400;
748 res.set_content(j.dump(), "application/json");
749 }
750}
751
752void HandleAnnotationUpdate(const httplib::Request& req, httplib::Response& res,
753 const std::string& project_path) {
754 ApplyCorsHeaders(res);
755
756 std::string path = ResolveAnnotationsPath(project_path);
757 if (path.empty()) {
758 json j;
759 j["error"] = "No project path configured";
760 res.status = 503;
761 res.set_content(j.dump(), "application/json");
762 return;
763 }
764
765 // Extract annotation ID from URL path
766 std::string annotation_id;
767 if (!req.matches.empty() && req.matches.size() > 1) {
768 annotation_id = req.matches[1].str();
769 }
770 if (annotation_id.empty()) {
771 json j;
772 j["error"] = "Missing annotation ID in URL";
773 res.status = 400;
774 res.set_content(j.dump(), "application/json");
775 return;
776 }
777
778 try {
779 json updated = json::parse(req.body);
780 json file_data = LoadAnnotationsFile(path);
781 json& annotations = file_data["annotations"];
782
783 bool found = false;
784 for (auto& ann : annotations) {
785 if (ann.value("id", "") == annotation_id) {
786 // Merge updated fields
787 for (auto it = updated.begin(); it != updated.end(); ++it) {
788 ann[it.key()] = it.value();
789 }
790 found = true;
791 break;
792 }
793 }
794
795 if (!found) {
796 json j;
797 j["error"] = "Annotation not found: " + annotation_id;
798 res.status = 404;
799 res.set_content(j.dump(), "application/json");
800 return;
801 }
802
803 if (SaveAnnotationsFile(path, file_data)) {
804 json response;
805 response["status"] = "ok";
806 response["id"] = annotation_id;
807 res.status = 200;
808 res.set_content(response.dump(), "application/json");
809 } else {
810 json j;
811 j["error"] = "Failed to write annotations file";
812 res.status = 500;
813 res.set_content(j.dump(), "application/json");
814 }
815 } catch (const std::exception& e) {
816 json j;
817 j["error"] = e.what();
818 res.status = 400;
819 res.set_content(j.dump(), "application/json");
820 }
821}
822
823void HandleAnnotationDelete(const httplib::Request& req, httplib::Response& res,
824 const std::string& project_path) {
825 ApplyCorsHeaders(res);
826 (void)req;
827
828 std::string path = ResolveAnnotationsPath(project_path);
829 if (path.empty()) {
830 json j;
831 j["error"] = "No project path configured";
832 res.status = 503;
833 res.set_content(j.dump(), "application/json");
834 return;
835 }
836
837 std::string annotation_id;
838 if (!req.matches.empty() && req.matches.size() > 1) {
839 annotation_id = req.matches[1].str();
840 }
841 if (annotation_id.empty()) {
842 json j;
843 j["error"] = "Missing annotation ID in URL";
844 res.status = 400;
845 res.set_content(j.dump(), "application/json");
846 return;
847 }
848
849 json file_data = LoadAnnotationsFile(path);
850 json& annotations = file_data["annotations"];
851
852 size_t original_size = annotations.size();
853 json filtered = json::array();
854 for (const auto& ann : annotations) {
855 if (ann.value("id", "") != annotation_id) {
856 filtered.push_back(ann);
857 }
858 }
859
860 if (filtered.size() == original_size) {
861 json j;
862 j["error"] = "Annotation not found: " + annotation_id;
863 res.status = 404;
864 res.set_content(j.dump(), "application/json");
865 return;
866 }
867
868 file_data["annotations"] = filtered;
869 if (SaveAnnotationsFile(path, file_data)) {
870 json response;
871 response["status"] = "ok";
872 response["id"] = annotation_id;
873 res.status = 200;
874 res.set_content(response.dump(), "application/json");
875 } else {
876 json j;
877 j["error"] = "Failed to write annotations file";
878 res.status = 500;
879 res.set_content(j.dump(), "application/json");
880 }
881}
882
883} // namespace api
884} // namespace cli
885} // namespace yaze
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:52
absl::StatusOr< RoomMetadata > GetDungeonRoomMetadata(int room_id)
absl::StatusOr< RenderResult > RenderDungeonRoom(const RenderRequest &req)
static CommandRegistry & Instance()
static ModelRegistry & GetInstance()
Provider for symbol (label) resolution in disassembly.
absl::StatusOr< std::string > ExportSymbols(SymbolFormat format) const
Export all symbols to a string in the specified format.
absl::StatusOr< float > ParseRenderScale(absl::string_view value)
bool IsTruthyParam(const std::string &value, bool param_present)
bool ParseRoomParam(const httplib::Request &req, httplib::Response &res, int &room_id)
void HandleWindowAction(const std::function< bool()> &action, const char *window_state, httplib::Response &res)
bool SaveAnnotationsFile(const std::string &path, const json &data)
std::string ResolveAnnotationsPath(const std::string &project_path)
void HandleStateUpdate(const httplib::Request &req, httplib::Response &res)
void ApplyCorsHeaders(httplib::Response &res)
void HandleCorsPreflight(const httplib::Request &req, httplib::Response &res)
void HandleEditorControl(const httplib::Request &req, httplib::Response &res, const std::function< absl::StatusOr< std::string >(const std::string &)> &action)
void HandleBreakpointHit(const httplib::Request &req, httplib::Response &res)
void HandleListModels(const httplib::Request &req, httplib::Response &res)
void HandleGetSymbols(const httplib::Request &req, httplib::Response &res, yaze::emu::debug::SymbolProvider *symbols)
void HandleNavigate(const httplib::Request &req, httplib::Response &res)
void HandleWindowShow(const httplib::Request &req, httplib::Response &res, const std::function< bool()> &action)
void HandleAnnotationUpdate(const httplib::Request &req, httplib::Response &res, const std::string &project_path)
void HandleRenderDungeonMetadata(const httplib::Request &req, httplib::Response &res, yaze::app::service::RenderService *render_service)
void HandleRenderDungeon(const httplib::Request &req, httplib::Response &res, yaze::app::service::RenderService *render_service)
void HandleCommandExecute(const httplib::Request &req, httplib::Response &res, yaze::Rom *rom)
void HandleAnnotationDelete(const httplib::Request &req, httplib::Response &res, const std::string &project_path)
nlohmann::json json
void HandleAnnotationList(const httplib::Request &req, httplib::Response &res, const std::string &project_path)
void HandleCommandExecuteDisabled(const httplib::Request &req, httplib::Response &res)
void HandleAnnotationCreate(const httplib::Request &req, httplib::Response &res, const std::string &project_path)
void HandleCommandList(const httplib::Request &req, httplib::Response &res)
void HandleWindowHide(const httplib::Request &req, httplib::Response &res, const std::function< bool()> &action)
void HandleHealth(const httplib::Request &req, httplib::Response &res, const BonjourPublisher *bonjour)
SymbolFormat
Supported symbol file formats.