10#include "absl/status/status.h"
17#include "nlohmann/json.hpp"
25using json = nlohmann::json;
38 std::string normalized = value;
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" ||
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);
58 const char* window_state, httplib::Response& res) {
64 response[
"status"] =
"ok";
65 response[
"window"] = window_state;
67 res.set_content(response.dump(),
"application/json");
72 response[
"status"] =
"error";
73 response[
"message"] =
"window action failed";
74 res.set_content(response.dump(),
"application/json");
80 response[
"status"] =
"error";
81 response[
"message"] =
"window control unavailable";
82 res.set_content(response.dump(),
"application/json");
87void HandleHealth(
const httplib::Request& req, httplib::Response& res,
93 j[
"service"] =
"yaze-agent-api";
97 j[
"discovery"] =
"bonjour";
99 j[
"discovery"] =
"none";
103 res.set_content(j.dump(),
"application/json");
109 const bool force_refresh =
110 IsTruthyParam(req.get_param_value(
"refresh"), req.has_param(
"refresh"));
111 auto models_or = registry.ListAllModels(force_refresh);
115 if (!models_or.ok()) {
117 j[
"error"] = models_or.status().message();
119 res.set_content(j.dump(),
"application/json");
123 json j_models = json::array();
124 for (
const auto& info : *models_or) {
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);
139 response[
"models"] = j_models;
140 response[
"count"] = j_models.size();
143 res.set_content(response.dump(),
"application/json");
150 std::string format_str = req.get_param_value(
"format");
151 if (format_str.empty())
152 format_str =
"mesen";
156 if (format_str ==
"mesen") {
158 }
else if (format_str ==
"asar") {
160 }
else if (format_str ==
"wla") {
162 }
else if (format_str ==
"bsnes") {
166 j[
"error"] =
"Unsupported symbol format";
167 j[
"format"] = format_str;
168 j[
"supported"] = {
"mesen",
"asar",
"wla",
"bsnes"};
170 res.set_content(j.dump(),
"application/json");
176 j[
"error"] =
"Symbol provider not available";
178 res.set_content(j.dump(),
"application/json");
183 if (export_or.ok()) {
184 const std::string accept = req.has_header(
"Accept")
185 ? req.get_header_value(
"Accept")
187 const bool wants_json =
188 accept.find(
"application/json") != std::string::npos;
191 j[
"symbols"] = *export_or;
192 j[
"format"] = format_str;
194 res.set_content(j.dump(),
"application/json");
197 res.set_content(*export_or,
"text/plain");
201 j[
"error"] = export_or.status().message();
203 res.set_content(j.dump(),
"application/json");
211 json body = json::parse(req.body);
212 uint32_t address = body.value(
"address", 0);
213 std::string source = body.value(
"source",
"unknown");
221 response[
"status"] =
"ok";
222 response[
"address"] = address;
223 response[
"message"] =
"Navigation request received";
225 res.set_content(response.dump(),
"application/json");
226 }
catch (
const std::exception& e) {
228 j[
"error"] = e.what();
230 res.set_content(j.dump(),
"application/json");
238 json body = json::parse(req.body);
239 uint32_t address = body.value(
"address", 0);
240 std::string source = body.value(
"source",
"unknown");
246 if (body.contains(
"cpu_state")) {
247 cpu_info = body[
"cpu_state"];
252 response[
"status"] =
"ok";
253 response[
"address"] = address;
255 res.set_content(response.dump(),
"application/json");
256 }
catch (
const std::exception& e) {
258 j[
"error"] = e.what();
260 res.set_content(j.dump(),
"application/json");
268 json body = json::parse(req.body);
275 response[
"status"] =
"ok";
277 res.set_content(response.dump(),
"application/json");
278 }
catch (
const std::exception& e) {
280 j[
"error"] = e.what();
282 res.set_content(j.dump(),
"application/json");
287 const std::function<
bool()>& action) {
289 HandleWindowAction(action,
"shown", res);
293 const std::function<
bool()>& action) {
295 HandleWindowAction(action,
"hidden", res);
314 if (!req.has_param(
"room")) {
316 j[
"error"] =
"Missing required parameter: room";
318 res.set_content(j.dump(),
"application/json");
321 const std::string s = req.get_param_value(
"room");
323 room_id = std::stoi(s,
nullptr, 0);
326 j[
"error"] =
"Invalid room parameter";
329 res.set_content(j.dump(),
"application/json");
341 if (!render_service) {
343 j[
"error"] =
"Render service not available";
345 res.set_content(j.dump(),
"application/json");
350 if (!ParseRoomParam(req, res, room_id))
356 if (req.has_param(
"overlays")) {
357 std::istringstream ss(req.get_param_value(
"overlays"));
359 while (std::getline(ss, tok,
',')) {
360 tok.erase(0, tok.find_first_not_of(
" \t"));
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")
384 if (req.has_param(
"scale")) {
385 const auto scale_or =
387 if (!scale_or.ok()) {
389 j[
"error"] = std::string(scale_or.status().message());
391 res.set_content(j.dump(),
"application/json");
400 render_req.
scale = scale;
403 if (!result_or.ok()) {
404 const auto& s = result_or.status();
406 j[
"error"] = std::string(s.message());
407 if (s.code() == absl::StatusCode::kInvalidArgument)
409 else if (s.code() == absl::StatusCode::kFailedPrecondition)
413 res.set_content(j.dump(),
"application/json");
417 const auto& result = *result_or;
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));
427 const httplib::Request& req, httplib::Response& res,
431 if (!render_service) {
433 j[
"error"] =
"Render service not available";
435 res.set_content(j.dump(),
"application/json");
440 if (!ParseRoomParam(req, res, room_id))
445 const auto& s = meta_or.status();
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");
453 const auto& meta = *meta_or;
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;
469 res.set_content(j.dump(),
"application/json");
477 const httplib::Request& req, httplib::Response& res,
478 const std::function<absl::StatusOr<std::string>(
const std::string&)>&
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") {
487 R
"({"protocol":"yaze-editor-control/v1","status":"error","error":"Local editor client required"})",
491 if (!action || req.body.size() > 16384) {
492 res.status = action ? 413 : 503;
494 R
"({"protocol":"yaze-editor-control/v1","status":"error","error":"Editor control unavailable or request too large"})",
498 auto result = action(req.body);
500 res.status = absl::IsDeadlineExceeded(result.status()) ? 504 : 503;
501 res.set_content(
json({{
"protocol",
"yaze-editor-control/v1"},
503 {
"error", std::string(result.status().message())}})
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");
518 json body = json::parse(req.body);
519 std::string command_name = body.value(
"command",
"");
520 if (command_name.empty()) {
522 j[
"error"] =
"Missing 'command' field";
524 res.set_content(j.dump(),
"application/json");
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>());
536 if (!registry.HasCommand(command_name)) {
538 j[
"error"] =
"Unknown command: " + command_name;
540 res.set_content(j.dump(),
"application/json");
544 std::string captured_output;
545 auto status = registry.Execute(command_name, args, rom, &captured_output);
549 response[
"status"] =
"ok";
550 response[
"command"] = command_name;
553 response[
"result"] = json::parse(captured_output);
555 response[
"result"] = captured_output;
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;
567 res.set_content(response.dump(),
"application/json");
568 }
catch (
const std::exception& e) {
570 j[
"error"] = e.what();
572 res.set_content(j.dump(),
"application/json");
577 httplib::Response& res) {
581 j[
"status"] =
"error";
582 j[
"error"] =
"Command execution is disabled on this server";
584 res.set_content(j.dump(),
"application/json");
592 json j_commands = json::array();
594 for (
const auto& category : registry.GetCategories()) {
595 for (
const auto& cmd_name : registry.GetCommandsInCategory(category)) {
596 const auto* meta = registry.GetMetadata(cmd_name);
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);
611 response[
"commands"] = j_commands;
612 response[
"count"] = j_commands.size();
614 res.set_content(response.dump(),
"application/json");
624 if (project_path.empty())
627 return project_path +
"/Docs/Dev/Planning/annotations.json";
632 return json::object();
633 std::ifstream file(path);
635 return json::object();
637 return json::parse(file);
639 return json::object();
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);
657 std::ofstream file(path);
660 file << data.dump(2);
667 const std::string& project_path) {
670 std::string path = ResolveAnnotationsPath(project_path);
673 j[
"error"] =
"No project path configured";
675 res.set_content(j.dump(),
"application/json");
679 json file_data = LoadAnnotationsFile(path);
680 json annotations = file_data.value(
"annotations", json::array());
683 if (req.has_param(
"room")) {
686 room_id = std::stoi(req.get_param_value(
"room"),
nullptr, 0);
689 j[
"error"] =
"Invalid room parameter";
691 res.set_content(j.dump(),
"application/json");
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);
701 annotations = filtered;
705 response[
"annotations"] = annotations;
707 res.set_content(response.dump(),
"application/json");
711 const std::string& project_path) {
714 std::string path = ResolveAnnotationsPath(project_path);
717 j[
"error"] =
"No project path configured";
719 res.set_content(j.dump(),
"application/json");
724 json new_annotation = json::parse(req.body);
725 json file_data = LoadAnnotationsFile(path);
727 if (!file_data.contains(
"annotations")) {
728 file_data[
"annotations"] = json::array();
730 file_data[
"annotations"].push_back(new_annotation);
732 if (SaveAnnotationsFile(path, file_data)) {
734 response[
"status"] =
"ok";
735 response[
"id"] = new_annotation.value(
"id",
"");
737 res.set_content(response.dump(),
"application/json");
740 j[
"error"] =
"Failed to write annotations file";
742 res.set_content(j.dump(),
"application/json");
744 }
catch (
const std::exception& e) {
746 j[
"error"] = e.what();
748 res.set_content(j.dump(),
"application/json");
753 const std::string& project_path) {
756 std::string path = ResolveAnnotationsPath(project_path);
759 j[
"error"] =
"No project path configured";
761 res.set_content(j.dump(),
"application/json");
766 std::string annotation_id;
767 if (!req.matches.empty() && req.matches.size() > 1) {
768 annotation_id = req.matches[1].str();
770 if (annotation_id.empty()) {
772 j[
"error"] =
"Missing annotation ID in URL";
774 res.set_content(j.dump(),
"application/json");
779 json updated = json::parse(req.body);
780 json file_data = LoadAnnotationsFile(path);
781 json& annotations = file_data[
"annotations"];
784 for (
auto& ann : annotations) {
785 if (ann.value(
"id",
"") == annotation_id) {
787 for (
auto it = updated.begin(); it != updated.end(); ++it) {
788 ann[it.key()] = it.value();
797 j[
"error"] =
"Annotation not found: " + annotation_id;
799 res.set_content(j.dump(),
"application/json");
803 if (SaveAnnotationsFile(path, file_data)) {
805 response[
"status"] =
"ok";
806 response[
"id"] = annotation_id;
808 res.set_content(response.dump(),
"application/json");
811 j[
"error"] =
"Failed to write annotations file";
813 res.set_content(j.dump(),
"application/json");
815 }
catch (
const std::exception& e) {
817 j[
"error"] = e.what();
819 res.set_content(j.dump(),
"application/json");
824 const std::string& project_path) {
828 std::string path = ResolveAnnotationsPath(project_path);
831 j[
"error"] =
"No project path configured";
833 res.set_content(j.dump(),
"application/json");
837 std::string annotation_id;
838 if (!req.matches.empty() && req.matches.size() > 1) {
839 annotation_id = req.matches[1].str();
841 if (annotation_id.empty()) {
843 j[
"error"] =
"Missing annotation ID in URL";
845 res.set_content(j.dump(),
"application/json");
849 json file_data = LoadAnnotationsFile(path);
850 json& annotations = file_data[
"annotations"];
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);
860 if (filtered.size() == original_size) {
862 j[
"error"] =
"Annotation not found: " + annotation_id;
864 res.set_content(j.dump(),
"application/json");
868 file_data[
"annotations"] = filtered;
869 if (SaveAnnotationsFile(path, file_data)) {
871 response[
"status"] =
"ok";
872 response[
"id"] = annotation_id;
874 res.set_content(response.dump(),
"application/json");
877 j[
"error"] =
"Failed to write annotations file";
879 res.set_content(j.dump(),
"application/json");
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
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.
constexpr uint32_t kTileTypes
constexpr uint32_t kCollision
constexpr uint32_t kSprites
constexpr uint32_t kCameraQuads
constexpr uint32_t kObjects
constexpr uint32_t kTrack
absl::StatusOr< float > ParseRenderScale(absl::string_view value)
constexpr const char * kCorsMaxAge
constexpr const char * kCorsAllowOrigin
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)
constexpr const char * kCorsAllowHeaders
constexpr const char * kCorsAllowMethods
json LoadAnnotationsFile(const std::string &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)
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.