132 const std::string& json_text) {
135 json = nlohmann::json::parse(json_text);
136 }
catch (
const nlohmann::json::parse_error& error) {
137 return absl::InvalidArgumentError(
138 absl::StrCat(
"Failed to parse proposal JSON: ", error.what()));
143 if (!json.contains(
"id") || !json[
"id"].is_string()) {
144 return absl::InvalidArgumentError(
145 "Proposal JSON must include string field 'id'");
147 proposal.
id = json[
"id"].get<std::string>();
149 if (!json.contains(
"prompt") || !json[
"prompt"].is_string()) {
150 return absl::InvalidArgumentError(
151 "Proposal JSON must include string field 'prompt'");
153 proposal.
prompt = json[
"prompt"].get<std::string>();
155 if (json.contains(
"ai_service") && json[
"ai_service"].is_string()) {
156 proposal.
ai_service = json[
"ai_service"].get<std::string>();
159 if (json.contains(
"reasoning") && json[
"reasoning"].is_string()) {
160 proposal.
reasoning = json[
"reasoning"].get<std::string>();
163 if (json.contains(
"status")) {
164 if (!json[
"status"].is_string()) {
165 return absl::InvalidArgumentError(
166 "Proposal 'status' must be a string value");
168 proposal.
status = ParseStatus(json[
"status"].get<std::string>());
173 if (json.contains(
"changes")) {
174 if (!json[
"changes"].is_array()) {
175 return absl::InvalidArgumentError(
176 "Proposal 'changes' field must be an array");
179 for (
const auto& change_json : json[
"changes"]) {
180 if (!change_json.is_object()) {
181 return absl::InvalidArgumentError(
182 "Each change entry must be a JSON object");
186 if (!change_json.contains(
"map_id") ||
187 !change_json[
"map_id"].is_number_integer()) {
188 return absl::InvalidArgumentError(
189 "Tile change missing integer field 'map_id'");
191 change.
map_id = change_json[
"map_id"].get<
int>();
193 if (!change_json.contains(
"x") ||
194 !change_json[
"x"].is_number_integer()) {
195 return absl::InvalidArgumentError(
196 "Tile change missing integer field 'x'");
198 change.
x = change_json[
"x"].get<
int>();
200 if (!change_json.contains(
"y") ||
201 !change_json[
"y"].is_number_integer()) {
202 return absl::InvalidArgumentError(
203 "Tile change missing integer field 'y'");
205 change.
y = change_json[
"y"].get<
int>();
208 ParseTileValue(change_json,
"old_tile"));
210 ParseTileValue(change_json,
"new_tile"));
212 proposal.
changes.push_back(change);
216 if (proposal.
changes.empty()) {
217 return absl::InvalidArgumentError(
218 "Proposal JSON did not include any tile16 changes");
221 proposal.
created_at = std::chrono::system_clock::now();
222 if (json.contains(
"created_at_ms") && json[
"created_at_ms"].is_number()) {
223 auto millis = json[
"created_at_ms"].get<int64_t>();
224 proposal.
created_at = std::chrono::system_clock::time_point(
225 std::chrono::milliseconds(millis));
243 const std::string& command,
247 std::vector<std::string> parts = absl::StrSplit(command,
' ');
249 if (parts.size() < 10) {
250 return absl::InvalidArgumentError(
251 absl::StrCat(
"Invalid command format: ", command));
254 if (parts[0] !=
"overworld" || parts[1] !=
"set-tile") {
255 return absl::InvalidArgumentError(
256 absl::StrCat(
"Not a set-tile command: ", command));
262 for (
size_t i = 2; i < parts.size(); i += 2) {
263 if (i + 1 >= parts.size())
break;
265 const std::string& flag = parts[i];
266 const std::string& value = parts[i + 1];
268 if (flag ==
"--map") {
269 change.
map_id = std::stoi(value);
270 }
else if (flag ==
"--x") {
271 change.
x = std::stoi(value);
272 }
else if (flag ==
"--y") {
273 change.
y = std::stoi(value);
274 }
else if (flag ==
"--tile") {
276 change.
new_tile =
static_cast<uint16_t
>(std::stoi(value,
nullptr, 16));
283 auto status = overworld.
Load(rom);
289 if (change.
map_id < 0x40) {
291 }
else if (change.
map_id < 0x80) {
306 const std::string& command,
310 std::vector<std::string> parts = absl::StrSplit(command,
' ');
312 if (parts.size() < 12) {
313 return absl::InvalidArgumentError(
314 absl::StrCat(
"Invalid set-area command format: ", command));
317 if (parts[0] !=
"overworld" || parts[1] !=
"set-area") {
318 return absl::InvalidArgumentError(
319 absl::StrCat(
"Not a set-area command: ", command));
322 int map_id = 0, x = 0, y = 0, width = 1, height = 1;
323 uint16_t new_tile = 0;
326 for (
size_t i = 2; i < parts.size(); i += 2) {
327 if (i + 1 >= parts.size())
break;
329 const std::string& flag = parts[i];
330 const std::string& value = parts[i + 1];
332 if (flag ==
"--map") {
333 map_id = std::stoi(value);
334 }
else if (flag ==
"--x") {
335 x = std::stoi(value);
336 }
else if (flag ==
"--y") {
337 y = std::stoi(value);
338 }
else if (flag ==
"--width") {
339 width = std::stoi(value);
340 }
else if (flag ==
"--height") {
341 height = std::stoi(value);
342 }
else if (flag ==
"--tile") {
343 new_tile =
static_cast<uint16_t
>(std::stoi(value,
nullptr, 16));
348 std::vector<Tile16Change> changes;
351 auto status = overworld.
Load(rom);
359 }
else if (map_id < 0x80) {
366 for (
int dy = 0; dy < height; ++dy) {
367 for (
int dx = 0; dx < width; ++dx) {
374 changes.push_back(change);
379 for (
int dy = 0; dy < height; ++dy) {
380 for (
int dx = 0; dx < width; ++dx) {
387 changes.push_back(change);
396 const std::string& command,
401 std::vector<std::string> parts = absl::StrSplit(command,
' ');
403 if (parts.size() < 8) {
404 return absl::InvalidArgumentError(
405 absl::StrCat(
"Invalid replace-tile command format: ", command));
408 if (parts[0] !=
"overworld" || parts[1] !=
"replace-tile") {
409 return absl::InvalidArgumentError(
410 absl::StrCat(
"Not a replace-tile command: ", command));
414 uint16_t old_tile = 0, new_tile = 0;
415 int x_min = 0, y_min = 0, x_max = 31, y_max = 31;
418 for (
size_t i = 2; i < parts.size(); i += 2) {
419 if (i + 1 >= parts.size())
break;
421 const std::string& flag = parts[i];
422 const std::string& value = parts[i + 1];
424 if (flag ==
"--map") {
425 map_id = std::stoi(value);
426 }
else if (flag ==
"--old-tile") {
427 old_tile =
static_cast<uint16_t
>(std::stoi(value,
nullptr, 16));
428 }
else if (flag ==
"--new-tile") {
429 new_tile =
static_cast<uint16_t
>(std::stoi(value,
nullptr, 16));
430 }
else if (flag ==
"--x-min") {
431 x_min = std::stoi(value);
432 }
else if (flag ==
"--y-min") {
433 y_min = std::stoi(value);
434 }
else if (flag ==
"--x-max") {
435 x_max = std::stoi(value);
436 }
else if (flag ==
"--y-max") {
437 y_max = std::stoi(value);
442 return absl::FailedPreconditionError(
443 "ROM must be loaded to scan for tiles to replace");
447 auto status = overworld.
Load(rom);
455 }
else if (map_id < 0x80) {
462 std::vector<Tile16Change> changes;
463 for (
int y = y_min; y <= y_max; ++y) {
464 for (
int x = x_min; x <= x_max; ++x) {
465 uint16_t current_tile = overworld.
GetTile(x, y);
466 if (current_tile == old_tile) {
473 changes.push_back(change);
478 if (changes.empty()) {
479 std::ostringstream oss;
480 oss <<
"0x" << std::hex << old_tile;
481 return absl::NotFoundError(
482 absl::StrCat(
"No tiles matching ", oss.str(),
" found in specified area"));
489 const std::string& prompt,
490 const std::vector<std::string>& commands,
491 const std::string& ai_service,
498 proposal.
created_at = std::chrono::system_clock::now();
502 for (
const auto& command : commands) {
504 if (command.empty() || command[0] ==
'#') {
509 if (absl::StrContains(command,
"overworld set-tile")) {
511 if (change_or.ok()) {
512 proposal.
changes.push_back(change_or.value());
514 return change_or.status();
516 }
else if (absl::StrContains(command,
"overworld set-area")) {
518 if (changes_or.ok()) {
520 changes_or.value().begin(),
521 changes_or.value().end());
523 return changes_or.status();
525 }
else if (absl::StrContains(command,
"overworld replace-tile")) {
527 if (changes_or.ok()) {
529 changes_or.value().begin(),
530 changes_or.value().end());
532 return changes_or.status();
537 if (proposal.
changes.empty()) {
538 return absl::InvalidArgumentError(
539 "No valid tile16 changes found in commands");
543 "Generated ", proposal.
changes.size(),
" tile16 changes from prompt");
588 if (!before_rom || !before_rom->
is_loaded()) {
589 return absl::FailedPreconditionError(
"Before ROM not loaded");
592 if (!after_rom || !after_rom->
is_loaded()) {
593 return absl::FailedPreconditionError(
"After ROM not loaded");
596 if (proposal.
changes.empty()) {
597 return absl::InvalidArgumentError(
"No changes to visualize");
601 int min_x = INT_MAX, min_y = INT_MAX;
602 int max_x = INT_MIN, max_y = INT_MIN;
603 int map_id = proposal.
changes[0].map_id;
605 for (
const auto& change : proposal.
changes) {
606 if (change.x < min_x) min_x = change.x;
607 if (change.y < min_y) min_y = change.y;
608 if (change.x > max_x) max_x = change.x;
609 if (change.y > max_y) max_y = change.y;
614 min_x = std::max(0, min_x - padding);
615 min_y = std::max(0, min_y - padding);
616 max_x = std::min(31, max_x + padding);
617 max_y = std::min(31, max_y + padding);
619 int width = (max_x - min_x + 1) * 16;
620 int height = (max_y - min_y + 1) * 16;
623 int diff_width = width * 2 + 8;
624 int diff_height = height;
626 std::vector<uint8_t> diff_data(diff_width * diff_height, 0x00);
627 gfx::Bitmap diff_bitmap(diff_width, diff_height, 8, diff_data);
633 auto before_status = before_overworld.
Load(before_rom);
634 if (!before_status.ok()) {
635 return before_status;
638 auto after_status = after_overworld.
Load(after_rom);
639 if (!after_status.ok()) {
647 }
else if (map_id < 0x80) {
664 for (
int y = min_y; y <= max_y; ++y) {
665 for (
int x = min_x; x <= max_x; ++x) {
666 uint16_t before_tile = before_overworld.
GetTile(x, y);
667 uint16_t after_tile = after_overworld.
GetTile(x, y);
669 bool is_changed = (before_tile != after_tile);
673 int pixel_x = (x - min_x) * 16;
674 int pixel_y = (y - min_y) * 16;
675 for (
int py = 0; py < 16; ++py) {
676 for (
int px = 0; px < 16; ++px) {
677 diff_bitmap.
SetPixel(pixel_x + px, pixel_y + py, color);
682 int right_offset = width + 8;
683 for (
int py = 0; py < 16; ++py) {
684 for (
int px = 0; px < 16; ++px) {
685 diff_bitmap.
SetPixel(right_offset + pixel_x + px, pixel_y + py, color);
692 for (
int y = 0; y < diff_height; ++y) {
693 for (
int x = 0; x < 8; ++x) {
694 diff_bitmap.
SetPixel(width + x, y, separator_color);